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

Dev.to (devto)

Dev.to (Forem) is an article provider: the post body is Markdown and the publish call creates an article with published: true. Rustiz talks to the Forem API with a personal API key. There is no developer app, no OAuth exchange, no scope model and no app review.

  • Rust source: crates/rustiz-providers/src/devto.rs
  • Upstream port: dev.to.provider.ts
  • Editor: Markdown
  • Connection mode: customFields

Overview

PropertyValue
Identifierdevto
Display nameDev.to
Connection modecustomFields (API key collected by the connect form / MCP)
Editormarkdown
Channel kindsocial (article content)
Comments / threadsNo. supports_comments = false; only the first PostDetails is published
Pending / async publishingNo. supports_pending = false; post() 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_job3
Character limit (max_length)100000 characters for the article body
API versionForem API v1 endpoints (/api/...), api-key header

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 nothing to register, so rustiz providers --status reports devto as configured even before any user has supplied an API key.

VariableRequiredDefaultPurpose
DEVTO_BASE_URLNohttps://dev.toOverrides the API origin. The value is trimmed of a trailing slash. Useful for a Forem instance or a mock server. Example: https://dev.to.
RESTRICT_UPLOAD_DOMAINSNounsetIf set, main_image.path must contain this substring. This is a deployment policy, not a Dev.to requirement.

required_env() purposefully does not include DEVTO_BASE_URL; it is only an optional override read through Config::base_url("DEVTO", ...).

Step-by-Step Developer App Setup

There is no developer portal application to create.

  1. Sign in to the Dev.to account that should publish the articles.

  2. Open Settings → Extensions: https://dev.to/settings/extensions.

  3. Click Generate API Key (the UI may call it Generate New Key).

  4. Copy the key. Treat it as a password: it can create, update and unpublish articles on that account. There are no scopes to select.

  5. Optionally verify it before connecting:

    curl -s https://dev.to/api/users/me \
      -H "api-key: $DEVTO_API_KEY"
    

    The response contains id, name, username and profile_image, which are exactly the fields Rustiz stores during authenticate.

Products and permissions

  • No products or APIs must be enabled.
  • No OAuth scopes exist for a personal API key.
  • The key is account-wide. There is no per-publication key.
  • To publish under an organization, the account must already be a member of that organization and the organization id must be supplied in the post settings.

Sandbox / testing mode

Dev.to has no sandbox environment. The API acts on the production account.

  • Use a secondary Dev.to account for tests, or accept that a test article is public.
  • Rustiz always sends published: true; there is no draft mode in this provider. The provider’s “schedule” is Rustiz waiting until the scheduled time before calling Dev.to.
  • A useful safe check is GET /api/users/me, which creates nothing.

Production app review

There is no app review. The same key works in production immediately. Dev.to’s general terms and per-endpoint rate limits still apply.

Connecting a Channel (CLI & MCP)

The credential payload has exactly one key:

KeyTypeValidation regexNotes
apiKeypassword/^.{3,}$/Dev.to personal API key

CLI

Create a JSON file (or pipe JSON on stdin with --fields -):

{ "apiKey": "dv_xxxxxxxxxxxxxxxxxxxxxxxx" }
rustiz channels connect devto --fields devto-key.json

# or without a file
printf '%s' '{"apiKey":"dv_xxxxxxxxxxxxxxxxxxxxxxxx"}' \
  | rustiz channels connect devto --fields -

A successful credential connection is immediate and returns:

{ "connected": true, "pending": false, "name": "Your Dev.to name" }

MCP

connectChannelTool arguments:

{
  "provider": "devto",
  "customFields": {
    "apiKey": "dv_xxxxxxxxxxxxxxxxxxxxxxxx"
  }
}

There is no follow-up step. Call integrationList to get the channel id. The returned connection object is {"type":"customFields","headless":true,...} in integrationSchema. Use --fields / customFields; --external-url / externalUrl is not the credential path for this provider.

Provider functions (triggerTool)

FunctionParametersReturnsNotes
tagsnone[{"value": <number>, "label": "<name>"}]Calls GET /api/tags?per_page=1000&page=1 with the stored key. value is the numeric Dev.to tag id.
organizationsnone[{"id": <number>, "name": "...", "username": "..."}]Reads up to 1000 of the user’s articles from /api/articles/me/all, collects distinct organization.username, then calls /api/organizations/{username} once per organization.

MCP examples:

{ "integrationId": "int_...", "methodName": "tags" }
{ "integrationId": "int_...", "methodName": "organizations" }

Both functions have an empty input_schema / dataSchema.

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. Also checked by validate_settings.
main_imageobjectNoCover image. Properties: id (string, required by the schema), path (string, required), alt (string), thumbnail (string URL). additionalProperties: true.
canonicalstringNoCanonical URL sent as canonical_url. If it contains (post: it is skipped by URL validation.
organizationstringNoDev.to organization id sent as organization_id. Leave empty to publish as the user.
tagsarrayYes (schema)maxItems: 4; each item needs value (number, tag id) and label (string, tag name). Only label is sent to Dev.to.

The schema marks title and tags as required. validate_settings is more lenient about a missing tags value (it treats it as an empty list), but the schema and normal editor flow require it.

Example object form (public API / CLI --json):

{
  "title": "Shipping a Rust scheduler",
  "main_image": { "id": "m1", "path": "https://cdn.example/cover.png" },
  "canonical": "https://blog.example/shipping-rust-scheduler",
  "organization": "1234",
  "tags": [
    { "value": 1, "label": "rust" },
    { "value": 2, "label": "api" }
  ]
}

MCP settings is an array of {key, value} pairs:

{
  "settings": [
    { "key": "title", "value": "Shipping a Rust scheduler" },
    { "key": "tags", "value": [
      { "value": 1, "label": "rust" },
      { "value": 2, "label": "api" }
    ] }
  ]
}

The engine adds "__type": "devto" automatically if it is missing.

validate_settings() rules

  1. title must exist and be a string; at least 2 Unicode scalar values.
  2. canonical, if present and a string, must be a valid URL unless it contains the internal (post: placeholder. The Rust URL check accepts an empty string, an http(s):// URL, or a www. host with a dot and a non-space tail.
  3. organization, if present and not null, must be a string.
  4. tags, if present and not null, must be an array with at most 4 items. Each item must be an object with a numeric value and a string label.
  5. main_image, if present and not null:
    • must be an object;
    • id must be a string;
    • path must be a string;
    • if RESTRICT_UPLOAD_DOMAINS is set, path must contain that value;
    • the path (query string stripped) must end in .png, .jpg, .jpeg, .gif, .webp or .mp4 (case-insensitive);
    • alt, if present, must be a string;
    • thumbnail, if present, must be a non-empty valid URL.

Media requirements

  • Dev.to only receives a cover image URL in main_image.path; the media / attachments array on the post is ignored.
  • The cover is not uploaded by Rustiz. It must already be a public URL.
  • The extension check allows .mp4, but Dev.to’s main_image is a cover image; a video URL will almost certainly be rejected by the platform.
  • Body images are whatever Markdown the post contains (Dev.to side-loads external images referenced in the article).

check_validity() is the trait default (Ok(None)). Dev.to has no additional media-count, dimension or file-type rule in this provider; the only media validation is the main_image extension/domain check in validate_settings().

Plugs

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

Implementation Notes & Gotchas

  • The article is always public. post() hard-codes published: true. There is no draft or native scheduling option. A scheduled post is only delayed by Rustiz; once the publish job runs, the article is live.
  • No Accept header. Forem’s v1 documentation says v1 requests should send Accept: application/vnd.forem.api-v1+json. Rustiz sends only api-key. The tests and ordinary api-key endpoints tolerate this, but if Dev.to returns an unexpected response or HTML, missing version negotiation is the first thing to check.
  • Organization id is sent as a JSON string. The schema and validation require organization to be a string, and post() sends "organization_id": "<string>". Dev.to’s article schema documents organization_id as an integer. If Dev.to rejects the organization, this type mismatch is the cause. Omitting organization avoids it.
  • (post: canonical placeholder is only skipped in validation. If a caller passes a string containing (post:, validate_settings accepts it, but post() still sends the non-empty value as canonical_url. Only omit the field to leave the canonical URL out.
  • Tag ids are replaced by labels. The tool returns {value: <id>, label: <name>}, but publishing sends only label. Renaming a tag in the editor changes what is sent; the numeric id is not used by Dev.to on create.
  • tags[].value must be a number in Rustiz. docs.postiz.com shows string values in some examples. The Rust validator explicitly requires Value::is_number; string tag values fail with each tag needs a numeric \value` (tag id)`.
  • organizations is an N+1 call and capped at 1000 articles. It reads /api/articles/me/all?per_page=1000; organizations that never appear on those articles are not listed. Large accounts can be slow.
  • No refresh. authenticate stores the key with a 100-year expires_in and refresh_token: Some(""). refresh_token() returns an empty access token with expires_in: 0. A genuine 401 therefore cannot be repaired by refresh; reconnect the channel with a fresh key.
  • 401 maps to refresh. The HTTP layer treats an unclassified 401 as RefreshToken (test unauthorized_is_a_refresh), so a revoked key shows up as a refresh/reconnect problem.
  • Canonical uniqueness. A 422 body containing Canonical url has already been taken is converted to the actionable message Canonical URL already exists.
  • Max concurrency 3. The provider limits itself to three simultaneous jobs. Dev.to also enforces its own per-endpoint limits (article creation is commonly limited to 10 requests per 30 seconds); the generic HTTP layer retries 429/500 up to three times with the configured 5-second delay.

References