Install
openclaw skills install @bitsandtea/postking-landing-pagesSEO Optimized Landing Pages. Main landing page and ICP- specific ones. Generate, edit, vibe-edit, and publish landing pages on PostKing, including comparison/side pages and custom domains, for launches and campaigns. Works with the SEO module to bring you 000's of impressions and visits organically!
openclaw skills install @bitsandtea/postking-landing-pagesHandles the landing-page lifecycle on PostKing: AI generation, targeted or full AI ("vibe") edits, side pages under a parent page, custom domains, and publishing. Requires an active brand — use the postking skill's brand-pick flow first if one isn't set.
Use this skill when the user wants to generate a new landing page, edit or AI-rewrite one, add or edit a side page (a comparison, text, or landing-style sub-page under a parent LP), attach a custom domain, or publish/preview a page.
postking-social.postking-blog.postking-seo. This skill covers generating and editing the pages themselves, including side pages fed by a roadmap brief.postking-brand-voice.postking-getting-started.list_landing_pages, generate_landing_page, view_landing_page, get_job — create and inspect (get_job is for the async generate_landing_page/regenerate_landing_page ops only — vibe-edit sessions are polled with get_vibe_edit_status, not get_job).edit_landing_page, set_landing_page_section, set_lp_section_layout, regenerate_landing_page, vibe_edit_landing_page, get_vibe_edit_status, apply_vibe_edit, restore_lp_version, delete_lp_version — editing.list_lp_versions, view_lp_version, view_lp_draft — landing-page version-history reads (restore_lp_version/delete_lp_version above are the writes) — see "Version history".list_side_pages, generate_side_page, import_side_page_html, view_side_page, edit_side_page, set_side_page_section, set_side_page_state — side pages.list_block_types, add_block, edit_block, delete_block, reorder_blocks — block-model editing for sidePageType: "custom" side pages — see "Custom side pages (block model)".list_side_page_versions, view_side_page_version, restore_side_page_version, delete_side_page_version — side-page version history — see "Version history".get_landing_page_translations, create_landing_page_translation — landing-page language variants — see "Landing page translations".list_asset_slots, assign_asset_to_slot — discover a page's asset slots and assign/reassign/clear the asset in a slot (works for main LPs and side pages — see "Assign or reassign an asset to a slot").add_domain, verify_domain, list_domains, delete_domain — custom domains (partial support — see Pitfalls).publish_landing_page, delete_landing_page — publish/lifecycle.generate_landing_page({ brandId, topic, slug?, language? }) — async; creates the LP record and kicks off AI content generation. language (en/es/pt-BR/de/fr/cs) overrides the brand's contentLanguage for this page only. Returns { slug, operationId, pollUrl }. Poll with get_job({ pollUrl: operationId, brandId, wait: true }) until state is completed (or failed/partially_failed/cancelled).view_landing_page({ slug }) — preview the generated content.publish_landing_page({ slug }) — free-tier choke point. Surface the returned webUrl to the user.edit_landing_page({ slug, title?, instructions? }).set_landing_page_section({ slug, section, field?, value, replaceSection? }) — each call creates a new draft version. For many related changes across a page, prefer the vibe-edit flow below. section is one of the 12 built-ins (hero, showcase, videos, howItWorks, features, categoryExplorer, replacesStack, comparisonMatrix, cta, faq, pricing, roiCalculator — the same list set_lp_section_layout orders) or a top-level root (slotMap, config, navigation, siteMetadata). Freeform HTML sections are a separate address: section: "customHtml" with field: "<id>" and value: { name?, html } (or replaceSection: true to replace the whole id -> {name?, html} map) — the HTML is sanitized server-side and rejected (not stripped) if it breaks the blk-*-class/no-script rules.set_lp_section_layout({ slug, sectionOrder?, sectionVisibility? }) — sectionOrder takes the 12 built-in keys above plus any customHtml:<id> freeform sections in the page.regenerate_landing_page({ slug, voiceProfileId?, instructions?, sections? }).vibe_edit_landing_page({ slug, instructions, scope?, sectionId?, language? }) — scope is "full" | "section" (sectionId required when scope: "section"; example section keys: hero, features, pricing, cta, faq, howItWorks, showcase, categoryExplorer, replacesStack, comparisonMatrix, roiCalculator). scope: "section" is hard-enforced: any patch keys outside sectionId are silently dropped, not applied elsewhere. Use scope: "full" for any edit that spans more than one section. language regenerates the edited content in that language for this edit only, overriding the brand's configured content language. Returns { operationId }.get_vibe_edit_status({ slug, operationId }) — not get_job, which cannot find vibe-edit sessions — until status is completed (or failed). The default detail: "medium" is the review view: { status, stale, changes: [{ index, path, before, after }] }. A structurally impossible request (e.g. asking for a 6th pricing plan when the schema caps at 5) fails fast with a clear error on the status response instead of looping/repairing — read the error and adjust the instructions rather than retrying the same payload.changes array, then apply_vibe_edit({ slug, operationId, all? | indices? | paths? }) to apply the accepted changes to the draft as one new version. A stale session (draft moved on since the edit was proposed) returns a stale_session error — re-run vibe_edit_landing_page or retry with force: true.publish_landing_page({ slug }) to make it live. restore_lp_version({ slug, versionId }) rolls the draft back if needed.pricing.plans)id, name, price (number ≥0), period, description, features (array of { text, included }), cta. Optional: isHighlighted, nofollow, contactSales, isComingSoon, outputs[], meta, metaItems[], valueProp, annualPrice, billingNote, ctaUrl, url, annualUrl, urls{monthly,annual}.url or ctaUrl (or urls.monthly/urls.annual for cadence-specific links) — don't invent a new key for it; it'll be rejected.Images and videos on a page are not stored inside section text — they live in a slotMap keyed by dotted slot keys (e.g. hero.backgroundVideo, cta.image, hero.logoCarousel, showcase.video.1, pricing.illustration). Each slot holds one asset ID, or an array of IDs for "array" slots. A main LP's slots are namespaced under a pageKey (main by default); a side page keeps its own separate slotMap.
list_assets, upload_asset, or import_asset_from_url. assign_asset_to_slot only references existing assets; it does not upload.list_asset_slots({ slug, sidePageSlug?, pageKey? }) — pass sidePageSlug for a side page. Returns each slot with its media type and current asset.assign_asset_to_slot({ slug, slotKey, assetId? | assetIds?, sidePageSlug?, pageKey?, clear? }). Synchronous — applies immediately, no polling.assign_asset_to_slot({ slug, slotKey: "hero.backgroundVideo", assetId }).assign_asset_to_slot({ slug, sidePageSlug, slotKey: "cta.image", assetId }).assign_asset_to_slot({ slug, slotKey: "hero.logoCarousel", clear: true }).Rules:
assetIds (plural); single slots require assetId. Using the wrong one is rejected.publish_landing_page({ slug }) to make it live. Side-page slotMap writes apply in place with no version of their own — unlike set_side_page_section/edit_side_page/the block tools, which do create one (see "Version history").slotMap shadows the parent LP's slots for that page.list_side_pages({ slug }) — see what already exists under a parent landing page. Each entry includes previewUrl (browser-openable draft preview, present whenever the server has a preview domain configured) and liveUrl (only when a custom domain is connected) — hand these to the user instead of constructing URLs yourself.generate_side_page({ slug, key, prompt?, brief?, keywords?, sidePageType?, name? }) — key is the required URL-slug fragment for the side page. sidePageType is "landing" | "text" | "comparison" | "custom" | "spotlight" (defaults to "landing") — there is no type param, and "comparison" requires a persisted briefId. "spotlight" is the feature / service spotlight page: a fixed-schema single-topic page (hero, a 3-6 step feature tour, 3-6 benefits, a stat + quote proof band, CTA footer) with no testimonial carousel, pricing table or showcase — use it whenever the page covers ONE feature or service, instead of "landing", which pulls in the full marketing template. Spotlight is freeform-only: pass key + prompt (+ optional name for the display title, keywords); brief and selectedSections are ignored for it. Async for most cases (returns operationId); some comparison briefs run synchronously and return sidePageId directly with no operationId. Poll get_job when an operationId is returned; the completed result includes previewUrl/liveUrl as above.import_side_page_html({ slug, url? | html?, sidePageSlug?, voiceProfileId?, autoAssignAssets? }) — clone an existing web page as a side page under the parent LP slug. Pass url (PostKing fetches the page server-side — don't download the HTML yourself; an invalid or SSRF-blocked URL 400s) or html directly, not both. sidePageSlug is optional with url (derived from the URL path), required with html. Behavior follows the parent LP's type: raw-HTML parent → synchronous verbatim import, returns { sidePage: { id, slug }, mode: "import", previewUrl?, liveUrl? }; sectioned parent → the source page's text is extracted and seeds an async side-page generation instead, returns { mode: "generate", operationId, key } — poll with get_job, same as generate_side_page. 409 = a side page with that slug already exists. Contrast with import_landing_page_html, which creates a standalone top-level landing page, not one nested under a parent.view_side_page({ slug, sideKey }) — inspect content and rendered HTML; also returns previewUrl/liveUrl.edit_side_page({ slug, sideKey, name?, newKey?, instructions?, updateReferences? }) — name sets the display title, which is what shows up in the parent landing page's auto-generated footer/nav "Solutions" links and in breadcrumbs; changing it updates those automatically. newKey renames the side page's URL-slug fragment — the old URL 404s afterward (no redirect), and existing internal references (blog backlinks, internal links) are rewritten in the background; poll the returned slugRewriteOperationId for that rewrite. updateReferences defaults to true and can be set false to skip that reference-rewrite cascade when renaming via newKey. instructions still only records a page-level annotation and does not itself trigger an AI edit. Use set_side_page_section({ slug, sideKey, sectionId, fields?, field?, value?, instructions? }) for a single structured section edit instead.set_side_page_state({ slug, sideKey, published }) — publish or unpublish it.A sidePageType: "custom" side page (see generate_side_page above) stores content as an ordered blocks[] array instead of the fixed section list "landing"/"spotlight" pages use — reach for it when the page needs a shape the fixed sections can't express. Every block tool addresses a specific side page with both the parent LP slug and the side page's sideKey (from list_side_pages) — there is no way to add blocks to a top-level landing page directly, only to a "custom"-type side page nested under one.
list_block_types({ brandId?, detail? }) — the entire runtime discovery mechanism for what add_block accepts: Tier 1 (the 12 built-in Uland sections — hero, showcase, videos, howItWorks, features, categoryExplorer, replacesStack, comparisonMatrix, cta, faq, pricing, roiCalculator) merged with Tier 2 (brand-visible BlockType rows seeded in the DB). detail: "full" returns each type's complete jsonSchema (and template for Tier 2) — call it before every add_block/edit_block to see exactly what shape props must have, rather than assuming a fixed catalog; a new Tier 2 type is usable immediately with no server rebuild. Tier 3 (type: "html") is always available and is not listed here.add_block({ slug, sideKey, type, props, position? }) — type is a key from list_block_types or the literal "html". props must match that type's schema exactly (a mismatch is rejected, not coerced, and the error names the offending field); for "html" it's { html: "<section class=\"blk-section\">...</section>" }, sanitized server-side against a blk-* class/tag allowlist (no <script>/<style>/<img>/inline standard CSS) and REJECTED with a reason on violation, never silently stripped. position is a 0-based insert index into the current array; omit to append. The server generates and returns the new block's id (blk_xxxxxxxx).edit_block({ slug, sideKey, blockId, type?, props? }) — props replaces the block's ENTIRE props object, not a shallow merge; fetch current props first via view_side_page({ slug, sideKey, detail: "full" }) (under overrides.blocks) if you're only changing one field, merge locally, then send the full object back. Passing type also revalidates props against the new type's schema — pass matching props in the same call.delete_block({ slug, sideKey, blockId, confirm: true }) — removes one block; recoverable via list_side_page_versions/restore_side_page_version even after deletion, since version history is append-only.reorder_blocks({ slug, sideKey, blockIds }) — blockIds must be the full, exact set of the page's current block ids in the new order; it cannot add or remove blocks (use add_block/delete_block for that), and a set mismatch is rejected.All four writes create a new draft version (draft-only until set_side_page_state({ published: true })) and are fenced by optimistic concurrency: a 409 means another write landed between your last read and this call — call view_side_page again to re-fetch current blocks and retry against the fresh state, don't blindly resend the same call.
Every content write to a landing page (set_landing_page_section, apply_vibe_edit, regenerate_landing_page, a main-LP assign_asset_to_slot) or to a side page (set_side_page_section, edit_side_page, the block tools above) creates a new draft version — publishing is always a separate step (publish_landing_page / set_side_page_state({ published: true })). The one exception is assign_asset_to_slot targeting a side page's slotMap, which writes in place with no version of its own.
list_lp_versions({ slug }) — history, each entry with a previewUrl. view_lp_version({ slug, versionId }) — inspect one version's section content. view_lp_draft({ slug }) — the current unpublished draft. restore_lp_version({ slug, versionId }) — roll the draft back to a prior version (does not touch the published version). delete_lp_version({ slug, versionId, confirm: true }) — permanently remove a historical version; the published version and the last remaining version can't be deleted.list_side_page_versions({ slug, sideKey }) — history; the response's top-level currentVersionId (draft) and publishedVersionId (live) tell you draft vs. live without a second call. view_side_page_version({ slug, sideKey, versionId }) — inspect a snapshot ({ type, overrides, siteMetadata, slotMap, config }). restore_side_page_version({ slug, sideKey, versionId }) — moves the draft forward to a new version copied from the target (forward history is never deleted); the live page is unaffected until you set_side_page_state({ published: true }) again. delete_side_page_version({ slug, sideKey, versionId, confirm: true }) — deleting the current draft version is allowed and repoints the draft to the newest remaining version; the published version and the last remaining version can't be deleted.A landing page can have language variants linked into a shared translation group — one page row per language, distinct from the brand's default contentLanguage. The target language must already be enabled for the brand (get_brand_languages/add_brand_language, in the postking skill) or create_landing_page_translation is rejected with a language-not-enabled error.
get_landing_page_translations({ slug }) — read the translation group slug belongs to: { slug, languageCode, pathPrefix, translationGroupId, isTranslationSource, variants: [...] }. variants lists the OTHER language variants already in the group (empty if none yet). Call this before create_landing_page_translation to check whether a variant in the target language already exists.create_landing_page_translation({ slug, language }) — slug must be the group's actual source page: fails with 400 if slug is itself already a translation (chain new variants off the source, never off another variant), and 409 if a variant in that language already exists in the group. Creates a new DRAFT page row (correct slug suffix and /{languageCode} path prefix) and kicks off an async job that pulls and translates the source page's current content; the response's operationId is pollable with get_job, and pullFromSourceUrl is a documented manual fallback (an AI-edit pull-from-source call) if you need to re-trigger the pull yourself.add_domain({ domain, primaryContentType: "landing_page", brandId? }) — registers the domain and returns a DNS TXT record to add at the registrar.verify_domain({ domainId }) — checks DNS once the record is added; domainId comes from the add_domain response or list_domains.connect_domain_to_publication only attaches a domain to a blog publication (it takes a publicationId, not a landing-page slug) — using it for a landing page is a no-op/wrong target, not a substitute. Tell the user to finish the connection from the landing page's own settings in the PostKing dashboard.delete_domain({ domainId }) — removes a custom domain; connected blogs and landing pages are unlinked but not deleted.| Goal | Command |
|---|---|
| List / generate | pking lp list / pking lp generate --topic "..." [--slug <slug>] [--voice <id>] |
| View / preview | pking lp view <slug> |
| Edit (metadata or AI pass) | pking lp edit <slug> [--title "..."] [--instructions "..."] |
| Vibe edit | pking lp vibe <slug> --instructions "..." [--scope full|section --section-id <id>] --wait then pking lp vibe status <slug> <operationId> |
| Side pages | pking lp side list <slug> / pking lp side edit <slug> <sideKey> --instructions "..." — generation is currently MCP-only, see Pitfalls |
| Side page section | `pking lp side section --id [--content "..." |
| Publish / unpublish a side page | pking lp side state <slug> <sideKey> --publish (or --unpublish) |
| Custom domain (register + verify only) | pking domains add <domain> → pking domains verify <domain> |
| Publish the landing page | pking lp publish <slug> |
For the full command catalog, use the postking skill's references/commands.md, or run pking lp --help.
generate_side_page has no type param — it's sidePageType, and the only valid values are "landing", "text", "comparison", "custom", "spotlight". Don't invent other values (e.g. "pricing", "features", "legal") as sidePageType — those aren't recognized side-page types by this tool. A feature page is "spotlight", not a made-up "features" value.key is required on generate_side_page — it's the side page's URL-slug fragment, not optional metadata.pking lp side generate <slug> --type <type> is currently broken — the inner route ignores the <slug> path param and requires name/slug/landingPageId in the request body, so it always 400s. Use the MCP tool generate_side_page({ slug, key, sidePageType }) instead.add_domain) and verifying (verify_domain) work via MCP/CLI, but attaching the verified domain to a specific landing page does not — don't tell the user connect_domain_to_publication will do it; that tool is blog-only.pking domains connect <id> --target lp:<slug> is not a working command — the CLI accepts the flag but the endpoint it calls does not exist on the server; don't suggest it.get_job (or the CLI's built-in --wait) rather than assuming failure early.sidePageId and no operationId — don't poll get_job with an ID you don't have; check the immediate response first.set_side_page_section, edit_side_page, and the block tools (add_block/edit_block/delete_block/reorder_blocks) each create a new draft version, and set_side_page_state({ published: true }) is the side-page equivalent of publish_landing_page — see "Version history". The one exception is assign_asset_to_slot targeting a side page's slotMap, which still writes in place with no version of its own — double-check slot assignments before writing.edit_side_page({ name, newKey })) — there is no pking CLI flag for it yet; pking lp side edit only supports --instructions.previewUrl or liveUrl is not an error. No liveUrl just means no custom domain is connected yet; no previewUrl means the server has no preview domain configured. Don't retry or report failure — just skip handing that link over.newKey changes the live URL with no redirect — the old URL 404s immediately. Warn the user before renaming a published side page, and mention that internal references are fixed up asynchronously (poll slugRewriteOperationId), not instantly.get_credits({ detail: "short" }) — confirms auth and balance.list_landing_pages({}) — returns the brand's landing pages (or an empty list if none created yet, which is expected pre-setup).