PostKing Blog
Handles the blog lifecycle on PostKing: publications (the containers articles live under), AI article generation, review/editing, and publishing — either to the PostKing-hosted blog or out to connected external platforms. Requires an active brand — use the postking skill's brand-pick flow first if one isn't set.
When to Use
Use this skill when the user wants to create or manage a blog publication; generate, review, edit, or rewrite a blog article; publish an article (hosted or external); check on an in-progress generation; or manage blog authors/categories.
When NOT to Use
- Social posts, content weeks, or repurposing to social →
postking-social. - Landing pages or side pages →
postking-landing-pages. - The SEO/GEO keyword-to-roadmap pipeline (seeds → keywords → clusters → briefs) that eventually produces briefs for articles →
postking-seo. This skill covers direct/manual blog generation and publishing, not that pipeline's brief-to-article automation. - Saved voice profile management or a standalone de-slop pass →
postking-brand-voice. - No active brand yet →
postking-getting-started.
Minimal tool subset
list_publications,create_publication,update_publication,delete_publication,list_blogs— publications.generate_blog_post,get_blog_status,get_job— async generation + polling.get_blog_article,update_blog_article,rewrite_text— review and iterate.list_assets,import_asset_from_url— pick or import an image to set as an article's featured/header image.publish_blog_article,list_publishing_connections,schedule_blog_article— publishing.list_blog_authors,create_blog_author,list_blog_categories,create_blog_category— metadata.import_blog_articles,delete_blog_article— housekeeping.
Procedure
Set up a publication
list_publications({ brandId? })(orlist_blogs, which also returns astatusBreakdownof existing articles) — find an existing publication.create_publication({ title, description?, layout?, languageCode?, brandId? })if none exists — returns apublicationId.languageCodeis optional (defaults to the brand's content language) and must already be enabled on the brand's language roster — see thepostkingskill's language section; an un-enabled code is rejected with a 403.delete_publication({ publicationId, brandId? })to remove one — only allowed when it has zero articles and no connected domain/external sync/publishing connections; the API refuses with an explanation of what's still attached otherwise (delete every article first withdelete_blog_article, disconnect domain/connections).
Generate a blog article
generate_blog_post({ publicationId, topic, voiceProfileId?, targetLength?, primaryKeywords?, secondaryKeywords?, brandId?, language? })— note the param names:primaryKeywords/secondaryKeywords, notkeywords;targetLength(short/medium/long), notlength.language(en/es/pt-BR/de/fr/cs) overrides the brand'scontentLanguagefor this article only. Async; returnsarticleId+operationId.- Poll
get_blog_status({ articleId }), orget_job({ pollUrl: operationId, brandId, wait: true }), until status iscompleted(orfailed). get_blog_article({ articleId })— review the draft (full content, SEO fields,previewUrl,editUrl, and the currentfeaturedImageUrl/featuredImageAlt/featuredImageDescription).
Review and iterate
update_blog_article({ articleId, title?, content?, excerpt?, status?, metaTitle?, metaDescription?, authorId?, categoryId?, featuredImageUrl?, featuredImageAlt?, featuredImageDescription?, cta?, sidePageInfo? })for direct edits — only the fields you pass are changed.rewrite_text({ text, voice?, platform: "blog" })to polish a specific passage, then paste the result back in viaupdate_blog_article.- Re-run
generate_blog_postwith a refinedtopic/primaryKeywordsfor a fresh pass instead of patching piecemeal changes.
Set or replace the featured (header) image
The header image can be set at generation time (generate_blog_post's generateAiImage/attachVisualAsset/selectedAssetId), but to add or swap it on an existing article:
- Get an image URL —
list_assets({ brandId? })and copy an asset'surl/fileUrl(returned as an absolute CDN URL, e.g.https://cdn.postking.app/assets/<brandId>/images/x.png); orimport_asset_from_url({ url })to pull an external image into the brand library first; or use any public image URL /data:URI directly. update_blog_article({ articleId, featuredImageUrl, featuredImageAlt?, featuredImageDescription? })— sets the header image. PassfeaturedImageUrl: ""to remove it. An external URL is auto-downloaded to brand storage when the article is published.get_blog_article({ articleId })— the response echoesfeaturedImageUrl/featuredImageAlt/featuredImageDescription, so you can confirm the change stuck.
Edit the CTA (call-to-action) block
The CTA lives in structured data (sidePageInfo on the article), not in the article body — never write CTA markup into content to change it; that gets stripped/ignored.
- Simple case —
update_blog_article({ articleId, cta: { url, label?, headline?, body? } }):url→ CTA link target (required when enabling a CTA)label→ CTA button textheadline→ CTA block headlinebody→ CTA block body copy
- Disable/remove the CTA —
update_blog_article({ articleId, cta: { enabled: false } })(equivalent tosidePageInfo: null). - Advanced case — link the CTA to an existing side page instead of a raw URL:
update_blog_article({ articleId, sidePageInfo: { id } })(id fromlist_side_pages).ctaandsidePageInfoare mutually exclusive — pass only one per call. - Example:
update_blog_article({ articleId: "art_123", cta: { url: "https://example.com/pricing", label: "See pricing", headline: "Ready to get started?", body: "Compare plans and find the right fit." } }) - Malformed CTA payloads are now rejected server-side (strict validation) instead of being silently persisted — pass exactly the documented fields (misspelled keys, e.g.
ctaUrlinstead ofurl, will error rather than silently doing nothing).
Publish
- PostKing-hosted:
update_blog_article({ articleId, status: "published" })makes it live on the brand's PostKing blog immediately. - Scheduled hosted publish:
schedule_blog_article({ articleId, scheduledAt })— future ISO 8601 datetime; auto-publishes (and auto-pushes to anyautoPublish-flagged external connections) when the time arrives. - External (WordPress / Medium / Substack, connected via the dashboard):
list_publishing_connections({ publicationId })to find connection IDs, thenpublish_blog_article({ articleId, connectionIds: [...] }). get_blog_status({ articleId })— confirm the final status and get the live URL(s).
CLI fast path
| Goal | Command |
|---|---|
| List publications / articles | pking blogs list [--status draft|published] / pking publications list |
| Create a publication | pking publications create --title "..." [--description "..."] |
| Generate a blog | pking blogs generate --publication <id> --topic "..." [--keywords "kw1,kw2"] [--voice <id>] [--length short|medium|long] --wait |
| Check generation status | pking blogs status <articleId> |
| View an article | pking blogs view <articleId> |
| Publish (hosted, or external via connections) | pking blogs publish <articleId> [--connections <id1,id2>] |
| Delete | pking blogs delete <articleId> |
For the full command catalog, use the postking skill's references/commands.md, or run pking blogs --help / pking publications --help.
Pitfalls
generate_blog_postparam names:primaryKeywords/secondaryKeywordsandtargetLength, notkeywords/length. Passing the wrong names silently drops them (extra zod fields are ignored, not errored).- Hosted publish vs. external publish are different tools.
publish_blog_articlerequires a non-emptyconnectionIdsarray — it's for pushing to external platforms, not for making the PostKing-hosted copy live. Useupdate_blog_article({ status: "published" })for that. - No
pking blogs auto-assign-ctaCLI command exists. If the user wants a CTA auto-assigned to a piece of content, that capability is the MCP toolseo_auto_assign_cta(owned by thepostking-seoskill's surface) — don't invent a CLI equivalent. - Generation is async and can take a couple of minutes. Poll
get_blog_statusorget_jobrather than assuming failure early;get_jobsupportswait: trueto block instead of polling manually. update_blog_articleis a partial update. Omitted fields are left unchanged — no need to resend the whole article to change one field.- The CTA is structured data, not article HTML. Use
cta/sidePageInfoonupdate_blog_article, never embed CTA markup incontent— it will be stripped on write, not rendered. Malformedcta/sidePageInfoshapes (unknown keys, empty{}, enabling without aurl) are rejected with a 400, not silently dropped. - The featured image is a URL/path, not an asset ID.
featuredImageUrltakes an image URL, a brand-asset URL (thefileUrlfromlist_assets— now an absolute CDN URL, e.g.https://cdn.postking.app/assets/<brandId>/images/x.png), or adata:URI — not an asset ID. Bare legacy paths (/assets/<brandId>/...) are still accepted as input for backward compatibility. (Contrast generation, wheregenerate_blog_postusesselectedAssetId, an asset ID.) Pass an empty string to clear it.
Verification
get_credits({ detail: "short" })— confirms auth and balance.list_publications({})— returns at least one publication once set up (or an empty list if none created yet, which is expected pre-setup).