SEO/GEO content engine. Seed, cluster, roadmap, brief, write, and publish — turning keywords into live articles. Delivers a compounding pipeline of organic traffic and AI-citation-friendly content, month over month.
Coding
PostKing · Landing Pages
Try itSEO 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!
What it does
Generate, edit, vibe-edit, and publish landing pages on PostKing, including comparison/side pages and custom domains, for launches and campaigns.
The skill document
PostKing Landing Pages
Handles 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.
When to Use
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.
When NOT to Use
- Social posts or content weeks →
postking-social. - Blog articles →
postking-blog. - The SEO/GEO roadmap pipeline that decides what pages to brief (seeds → keywords → clusters → roadmap items) →
postking-seo. This skill covers generating and editing the pages themselves, including side pages fed by a roadmap brief. - Saved voice profile management or a standalone de-slop pass →
postking-brand-voice. - No active brand yet →
postking-getting-started.
Minimal tool subset
list_landing_pages,generate_landing_page,view_landing_page,get_job— create and inspect (get_jobis for the asyncgenerate_landing_page/regenerate_landing_pageops only — vibe-edit sessions are polled withget_vibe_edit_status, notget_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_versionabove 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 forsidePageType: "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.
Procedure
Generate and publish a landing page
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'scontentLanguagefor this page only. Returns{ slug, operationId, pollUrl }. Poll withget_job({ pollUrl: operationId, brandId, wait: true })untilstateiscompleted(orfailed/partially_failed/cancelled).view_landing_page({ slug })— preview the generated content.publish_landing_page({ slug })— free-tier choke point. Surface the returnedwebUrlto the user.
Edit a landing page
- Targeted metadata/instructions patch:
edit_landing_page({ slug, title?, instructions? }). - Direct field/section write (no AI):
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.sectionis one of the 12 built-ins (hero,showcase,videos,howItWorks,features,categoryExplorer,replacesStack,comparisonMatrix,cta,faq,pricing,roiCalculator— the same listset_lp_section_layoutorders) or a top-level root (slotMap,config,navigation,siteMetadata). Freeform HTML sections are a separate address:section: "customHtml"withfield: ""andvalue: { name?, html }(orreplaceSection: trueto replace the wholeid -> {name?, html}map) — the HTML is sanitized server-side and rejected (not stripped) if it breaks theblk-*-class/no-script rules. - Reorder or toggle visibility of sections:
set_lp_section_layout({ slug, sectionOrder?, sectionVisibility? })—sectionOrdertakes the 12 built-in keys above plus anycustomHtml:freeform sections in the page. - Full AI regeneration pass, optionally scoped to specific sections:
regenerate_landing_page({ slug, voiceProfileId?, instructions?, sections? }). - Natural-language AI edit ("vibe edit") — this is a propose → review → apply flow, nothing changes until you apply it:
vibe_edit_landing_page({ slug, instructions, scope?, sectionId?, language? })—scopeis"full" | "section"(sectionIdrequired whenscope: "section"; example section keys:hero,features,pricing,cta,faq,howItWorks,showcase,categoryExplorer,replacesStack,comparisonMatrix,roiCalculator).scope: "section"is hard-enforced: any patch keys outsidesectionIdare silently dropped, not applied elsewhere. Usescope: "full"for any edit that spans more than one section.languageregenerates the edited content in that language for this edit only, overriding the brand's configured content language. Returns{ operationId }.- Poll
get_vibe_edit_status({ slug, operationId })— notget_job, which cannot find vibe-edit sessions — untilstatusiscompleted(orfailed). The defaultdetail: "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. - Review the
changesarray, thenapply_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 astale_sessionerror — re-runvibe_edit_landing_pageor retry withforce: true. publish_landing_page({ slug })to make it live.restore_lp_version({ slug, versionId })rolls the draft back if needed.
Pricing section (pricing.plans)
- 1–5 plans max, no more. A request for a 6th plan is structurally impossible and fails fast (see step 2 above).
- Plan objects are strict — unknown keys are rejected. Required:
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}. - Put a plan's checkout/signup link in
urlorctaUrl(orurls.monthly/urls.annualfor cadence-specific links) — don't invent a new key for it; it'll be rejected.
Assign or reassign an asset to a slot
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.
- Make sure the asset is in the brand library first —
list_assets,upload_asset, orimport_asset_from_url.assign_asset_to_slotonly references existing assets; it does not upload. - Discover slot keys and their current assignments:
list_asset_slots({ slug, sidePageSlug?, pageKey? })— passsidePageSlugfor a side page. Returns each slot with its media type and current asset. - Assign, reassign, or clear the slot:
assign_asset_to_slot({ slug, slotKey, assetId? | assetIds?, sidePageSlug?, pageKey?, clear? }). Synchronous — applies immediately, no polling.
- Main landing page:
assign_asset_to_slot({ slug, slotKey: "hero.backgroundVideo", assetId }). - Side page:
assign_asset_to_slot({ slug, sidePageSlug, slotKey: "cta.image", assetId }). - Clear a slot:
assign_asset_to_slot({ slug, slotKey: "hero.logoCarousel", clear: true }).
Rules:
- Array slots require
assetIds(plural); single slots requireassetId. Using the wrong one is rejected. - The asset must belong to the brand and match the slot's media type (IMAGE vs VIDEO), or the call is rejected.
- A main-LP assignment creates a new draft version —
publish_landing_page({ slug })to make it live. Side-pageslotMapwrites apply in place with no version of their own — unlikeset_side_page_section/edit_side_page/the block tools, which do create one (see "Version history"). - A side page carrying its own
slotMapshadows the parent LP's slots for that page.
Side pages
list_side_pages({ slug })— see what already exists under a parent landing page. Each entry includespreviewUrl(browser-openable draft preview, present whenever the server has a preview domain configured) andliveUrl(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? })—keyis the required URL-slug fragment for the side page.sidePageTypeis"landing" | "text" | "comparison" | "custom" | "spotlight"(defaults to"landing") — there is notypeparam, and"comparison"requires a persistedbriefId."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: passkey+prompt(+ optionalnamefor the display title,keywords);briefandselectedSectionsare ignored for it. Async for most cases (returnsoperationId); some comparison briefs run synchronously and returnsidePageIddirectly with nooperationId. Pollget_jobwhen anoperationIdis returned; the completed result includespreviewUrl/liveUrlas above.import_side_page_html({ slug, url? | html?, sidePageSlug?, voiceProfileId?, autoAssignAssets? })— clone an existing web page as a side page under the parent LPslug. Passurl(PostKing fetches the page server-side — don't download the HTML yourself; an invalid or SSRF-blocked URL 400s) orhtmldirectly, not both.sidePageSlugis optional withurl(derived from the URL path), required withhtml. 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 withget_job, same asgenerate_side_page. 409 = a side page with that slug already exists. Contrast withimport_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 returnspreviewUrl/liveUrl.edit_side_page({ slug, sideKey, name?, newKey?, instructions?, updateReferences? })—namesets 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.newKeyrenames 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 returnedslugRewriteOperationIdfor that rewrite.updateReferencesdefaults totrueand can be setfalseto skip that reference-rewrite cascade when renaming vianewKey.instructionsstill only records a page-level annotation and does not itself trigger an AI edit. Useset_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.
Custom side pages (block model)
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 whatadd_blockaccepts: 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-visibleBlockTyperows seeded in the DB).detail: "full"returns each type's completejsonSchema(andtemplatefor Tier 2) — call it before everyadd_block/edit_blockto see exactly what shapepropsmust 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? })—typeis a key fromlist_block_typesor the literal"html".propsmust match that type's schema exactly (a mismatch is rejected, not coerced, and the error names the offending field); for"html"it's{ html: "..." }, sanitized server-side against ablk-*class/tag allowlist (no//``/inline standard CSS) and REJECTED with a reason on violation, never silently stripped.positionis 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? })—propsreplaces the block's ENTIRE props object, not a shallow merge; fetch current props first viaview_side_page({ slug, sideKey, detail: "full" })(underoverrides.blocks) if you're only changing one field, merge locally, then send the full object back. Passingtypealso revalidatespropsagainst the new type's schema — pass matchingpropsin the same call.delete_block({ slug, sideKey, blockId, confirm: true })— removes one block; recoverable vialist_side_page_versions/restore_side_page_versioneven after deletion, since version history is append-only.reorder_blocks({ slug, sideKey, blockIds })—blockIdsmust be the full, exact set of the page's current block ids in the new order; it cannot add or remove blocks (useadd_block/delete_blockfor 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.
Version history
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.
- Landing page:
list_lp_versions({ slug })— history, each entry with apreviewUrl.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. - Side page:
list_side_page_versions({ slug, sideKey })— history; the response's top-levelcurrentVersionId(draft) andpublishedVersionId(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 youset_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.
Landing page translations
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 groupslugbelongs to:{ slug, languageCode, pathPrefix, translationGroupId, isTranslationSource, variants: [...] }.variantslists the OTHER language variants already in the group (empty if none yet). Call this beforecreate_landing_page_translationto check whether a variant in the target language already exists.create_landing_page_translation({ slug, language })—slugmust be the group's actual source page: fails with 400 ifslugis 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'soperationIdis pollable withget_job, andpullFromSourceUrlis a documented manual fallback (an AI-edit pull-from-source call) if you need to re-trigger the pull yourself.
Custom domain (partial support)
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;domainIdcomes from theadd_domainresponse orlist_domains.- There is currently no MCP or CLI tool to connect a verified domain to a specific landing page.
connect_domain_to_publicationonly attaches a domain to a blog publication (it takes apublicationId, 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.
CLI fast path
| Goal | Command |
|---|---|
| List / generate | pking lp list / pking lp generate --topic "..." [--slug ] [--voice ] |
| View / preview | pking lp view |
| Edit (metadata or AI pass) | pking lp edit [--title "..."] [--instructions "..."] |
| Vibe edit | pking lp vibe --instructions "..." [--scope full|section --section-id ] --wait then pking lp vibe status |
| Side pages | pking lp side list / pking lp side edit --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 --publish (or --unpublish) |
| Custom domain (register + verify only) | pking domains add → pking domains verify |
| Publish the landing page | pking lp publish |
For the full command catalog, use the postking skill's references/commands.md, or run pking lp --help.
Pitfalls
generate_side_pagehas notypeparam — it'ssidePageType, and the only valid values are"landing","text","comparison","custom","spotlight". Don't invent other values (e.g. "pricing", "features", "legal") assidePageType— those aren't recognized side-page types by this tool. A feature page is"spotlight", not a made-up"features"value.keyis required ongenerate_side_page— it's the side page's URL-slug fragment, not optional metadata.pking lp side generate --typeis currently broken — the inner route ignores the `` path param and requiresname/slug/landingPageIdin the request body, so it always 400s. Use the MCP toolgenerate_side_page({ slug, key, sidePageType })instead.- Custom domains for landing pages stop short of dashboard-only. Registering (
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 userconnect_domain_to_publicationwill do it; that tool is blog-only. pking domains connect --target lp: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.- Async operations can take 30s–5min. Poll
get_job(or the CLI's built-in--wait) rather than assuming failure early. - Comparison-type side pages may return synchronously with
sidePageIdand nooperationId— don't pollget_jobwith an ID you don't have; check the immediate response first. - Side-page content writes DO have version history and a draft/publish split, the same shape as the main LP:
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, andset_side_page_state({ published: true })is the side-page equivalent ofpublish_landing_page— see "Version history". The one exception isassign_asset_to_slottargeting a side page'sslotMap, which still writes in place with no version of its own — double-check slot assignments before writing. - Renaming a side page's title or key is MCP-only (
edit_side_page({ name, newKey })) — there is nopkingCLI flag for it yet;pking lp side editonly supports--instructions. - A missing
previewUrlorliveUrlis not an error. NoliveUrljust means no custom domain is connected yet; nopreviewUrlmeans the server has no preview domain configured. Don't retry or report failure — just skip handing that link over. newKeychanges 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 (pollslugRewriteOperationId), not instantly.
Verification
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).
Related skills
SEO/GEO content engine. Seed, cluster, roadmap, brief, write, and publish — turning keywords into live articles. Delivers a compounding pipeline of organic traffic and AI-citation-friendly content, month over month.
Generate a complete week of social media content for all platforms from a single topic or brand. Produces platform-optimized posts, a beautiful shareable HTML content calendar, hashtag suggestions, posting-time recommendations, and JSON export for scheduling tools.
Router skill for PostKing — confirms the active brand, then hands off to the right specialist skill for posts, blogs, landing pages, SEO, or setup.
Know exactly who you're up against. Discover, classify, and track competitors and pull comparison data. Delivers a live competitor map and comparison pages that sharpen your positioning and win side-by-side searches.
Publish blogs that build authority and traffic. Generate, refine, and ship articles PostKing-hosted or pushed to WordPress, Medium, or Substack. Delivers a steady stream of SEO-ready articles without the writer's block.