{
  "schemaVersion": "1.0",
  "updatedAt": "2026-07-17",
  "canonical": "https://seodrafts.com/integrations/catalog.json",
  "documentation": "https://seodrafts.com/docs/integration.html",
  "integrations": [
    {
      "id": "wordpress",
      "name": "WordPress",
      "provider": "wordpress",
      "adapter": "wordpress",
      "aliases": [],
      "type": "cms",
      "status": "stable",
      "recommendedWave": 1,
      "docsUrl": "https://seodrafts.com/integrations/wordpress/",
      "setupKind": "preview",
      "setupCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nWORDPRESS_URL='<wordpress-url>' \\\nWORDPRESS_USERNAME='<wordpress-username>' \\\nWORDPRESS_APP_PASSWORD='<wordpress-app-password>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli publish-cms --cms wordpress --site-slug '<slug>' --site-origin '<origin>' --dry-run",
      "healthMode": "cms",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nWORDPRESS_URL='<wordpress-url>' \\\nWORDPRESS_USERNAME='<wordpress-username>' \\\nWORDPRESS_APP_PASSWORD='<wordpress-app-password>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nSEO_BLOG_SCHEDULED='true' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode cms --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "WORDPRESS_URL",
        "WORDPRESS_USERNAME",
        "WORDPRESS_APP_PASSWORD",
        "SEO_BLOG_SITE_ORIGIN"
      ],
      "bestFor": "Existing WordPress blogs that want a structured research, briefing, review, and publishing workflow without moving to a hosted content subdomain.",
      "authentication": "A dedicated WordPress user and Application Password. Basic authentication is sent only from the server-side publisher to the WordPress REST API.",
      "publishingFlow": [
        "Fetch only approved posts that are due for this site.",
        "Find an existing post by slug so repeat runs update instead of duplicate.",
        "Write title, slug, HTML body, excerpt, and configured SEO fields.",
        "Mark the item published only after WordPress returns a plausible public URL."
      ],
      "fields": [
        "Native title, slug, content, and excerpt fields",
        "Optional SEO metadata through configured post meta",
        "Explicit draft and publish modes"
      ],
      "canonicalOwnership": "WordPress and the active theme or SEO plugin remain responsible for the permalink, canonical tag, sitemap, and rendering. SEODrafts stores the final WordPress URL rather than creating a parallel proxy page.",
      "mediaHandling": "The body is transferred as HTML. Images need a durable public URL or a separate step that uploads them to the WordPress media library before publication.",
      "limitations": [
        "Custom fields need REST access when a plugin consumes them.",
        "An Application Password must never enter a browser bundle or prompt.",
        "Page-builder-specific fields need a custom mapping or webhook."
      ],
      "recovery": [
        "Run a dry run and verify the derived URL first.",
        "For 401 responses, check HTTPS origin, user, and Application Password.",
        "For layout problems, inspect the WordPress post type and theme template."
      ]
    },
    {
      "id": "webflow",
      "name": "Webflow",
      "provider": "webflow",
      "adapter": "webflow",
      "aliases": [],
      "type": "cms",
      "status": "stable",
      "recommendedWave": 1,
      "docsUrl": "https://seodrafts.com/integrations/webflow/",
      "setupKind": "preview",
      "setupCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nWEBFLOW_TOKEN='<webflow-token>' \\\nWEBFLOW_COLLECTION_ID='<webflow-collection-id>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli publish-cms --cms webflow --site-slug '<slug>' --site-origin '<origin>' --dry-run",
      "healthMode": "cms",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nWEBFLOW_TOKEN='<webflow-token>' \\\nWEBFLOW_COLLECTION_ID='<webflow-collection-id>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nSEO_BLOG_SCHEDULED='true' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode cms --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "WEBFLOW_TOKEN",
        "WEBFLOW_COLLECTION_ID",
        "SEO_BLOG_SITE_ORIGIN"
      ],
      "bestFor": "Marketing sites with a tested Webflow CMS Collection and an existing Collection Template for articles.",
      "authentication": "A Webflow API token scoped to the destination Collection. The token and Collection ID remain in CI or another server-side secret store.",
      "publishingFlow": [
        "Read Collection items and locate the destination slug.",
        "Map title, body, excerpt, and SEO fields to the site schema.",
        "Patch an existing item or create a controlled draft.",
        "Send only confirmed item IDs to the Collection publish endpoint."
      ],
      "fields": [
        "Rich text receives the rendered body HTML",
        "Custom collection fields use an explicit field map",
        "Existing items are updated rather than recreated"
      ],
      "canonicalOwnership": "The Webflow Collection Template owns the public route, canonical tag, and sitemap. SEODrafts checks the expected URL against the configured site origin, while the rendered Webflow output remains authoritative.",
      "mediaHandling": "Webflow file fields are not populated from arbitrary data URLs. Use a durable public asset or a dedicated upload step and map the resulting field explicitly.",
      "limitations": [
        "Default mappings only fit collections with matching field names.",
        "Large collections need a deterministic lookup strategy.",
        "The API token needs publishing rights for the target site and Collection."
      ],
      "recovery": [
        "Begin with dry run and draft mode.",
        "Compare the field map with Webflow field types when validation fails.",
        "Verify the Collection Template route before reporting a post as published."
      ]
    },
    {
      "id": "framer",
      "name": "Framer",
      "provider": "framer",
      "adapter": "framer",
      "aliases": [
        "Framer Sites",
        "Framer CMS"
      ],
      "type": "cms",
      "status": "beta",
      "recommendedWave": 1,
      "docsUrl": "https://seodrafts.com/integrations/framer/",
      "setupKind": "preview",
      "setupCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nFRAMER_API_KEY='<framer-api-key>' \\\nFRAMER_PROJECT_URL='<framer-project-url>' \\\nFRAMER_COLLECTION='<framer-collection>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli publish-cms --cms framer --site-slug '<slug>' --site-origin '<origin>' --dry-run",
      "healthMode": "cms",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nFRAMER_API_KEY='<framer-api-key>' \\\nFRAMER_PROJECT_URL='<framer-project-url>' \\\nFRAMER_COLLECTION='<framer-collection>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nSEO_BLOG_SCHEDULED='true' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode cms --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "FRAMER_API_KEY",
        "FRAMER_PROJECT_URL",
        "FRAMER_COLLECTION",
        "SEO_BLOG_SITE_ORIGIN"
      ],
      "bestFor": "Framer sites with a writable CMS Collection and teams that can test the beta adapter in a controlled Node.js 22+ environment.",
      "authentication": "A Framer API key, complete project URL, and Collection name or ID stored in the server-side publisher environment.",
      "publishingFlow": [
        "Connect to the project and reject read-only Collections.",
        "Map title, body, excerpt, SEO, author, date, and image fields.",
        "Write typed values with a stable item identifier.",
        "Publish, verify the deployment ID, deploy, and always disconnect."
      ],
      "fields": [
        "Stable item ID derives from the internal post ID",
        "Slug or stable ID prevents accidental duplicates",
        "A missing field match blocks the run"
      ],
      "canonicalOwnership": "The Framer site and CMS detail template own the route, canonical tag, and sitemap. The adapter writes content and starts deployment; it does not replace Framer SEO settings.",
      "mediaHandling": "Image fields receive an existing durable URL. Local files and data URLs need a separate asset host before the beta adapter can reference them.",
      "limitations": [
        "Requires Node.js 22+ and the optional Framer package.",
        "Server API behavior can change while the adapter is beta.",
        "Read-only Collections or a missing deployment ID block publication."
      ],
      "recovery": [
        "Identify the Collection and document the field map in dry run.",
        "Compare field names and types when no fields match.",
        "Never mark a failed deployment published manually; rerun with the stable post ID."
      ]
    },
    {
      "id": "wix",
      "name": "Wix",
      "provider": "wix",
      "adapter": "wix",
      "aliases": [
        "Wix Studio",
        "Wix Blog"
      ],
      "type": "cms",
      "status": "stable",
      "recommendedWave": 2,
      "docsUrl": "https://seodrafts.com/integrations/wix/",
      "setupKind": "preview",
      "setupCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nWIX_API_KEY='<wix-api-key>' \\\nWIX_SITE_ID='<wix-site-id>' \\\nWIX_MEMBER_ID='<wix-member-id>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli publish-cms --cms wix --site-slug '<slug>' --site-origin '<origin>' --dry-run",
      "healthMode": "cms",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nWIX_API_KEY='<wix-api-key>' \\\nWIX_SITE_ID='<wix-site-id>' \\\nWIX_MEMBER_ID='<wix-member-id>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nSEO_BLOG_SCHEDULED='true' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode cms --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "WIX_API_KEY",
        "WIX_SITE_ID",
        "WIX_MEMBER_ID",
        "SEO_BLOG_SITE_ORIGIN"
      ],
      "bestFor": "Wix sites using Wix Blog with an API member configured as the author for approved posts.",
      "authentication": "A Wix API key plus Site ID and Member ID. The site context is sent in the API request and the member becomes the post author.",
      "publishingFlow": [
        "Split large Markdown into safe blocks and convert each to Ricos.",
        "Merge the returned nodes into one rich-content document.",
        "Update an existing draft by SEO slug or title when possible.",
        "Write SEO data and call the publish endpoint only after approval."
      ],
      "fields": [
        "Excerpt is capped at 500 characters",
        "SEO slug is capped at 100 characters",
        "Project locale maps to the Wix language code"
      ],
      "canonicalOwnership": "Wix Blog generates the public route and canonical tag. SEODrafts records the expected URL from the site origin and slug but waits for the successful Wix response before treating it as final.",
      "mediaHandling": "The current adapter focuses on rich content and SEO data. Featured media needs a Wix-compatible asset URL or a separate media workflow.",
      "limitations": [
        "The publisher blocks content above its safe payload limit.",
        "Every Markdown block must return a valid Ricos document.",
        "API key, site, and member must belong to the same Wix context."
      ],
      "recovery": [
        "Publish as a draft and inspect rich content first.",
        "When an ID is missing, inspect the API response instead of creating blindly again.",
        "Isolate the Markdown block responsible for conversion errors."
      ]
    },
    {
      "id": "markdown-mdx",
      "name": "Markdown/MDX + Git",
      "provider": "static",
      "adapter": "static",
      "aliases": [
        "Next.js",
        "Astro",
        "Nuxt",
        "SvelteKit",
        "Remix",
        "Hugo",
        "Gatsby",
        "Docusaurus",
        "VitePress"
      ],
      "type": "git",
      "status": "stable",
      "recommendedWave": 1,
      "docsUrl": "https://seodrafts.com/integrations/markdown-mdx/",
      "setupKind": "initializer",
      "setupCommand": "SEO_BLOG_SITE_SLUG='<slug>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli init-static --site-slug '<slug>' --site-origin '<origin>' --content-dir '<content-dir>' --extension mdx",
      "healthMode": "static",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode static --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "SEO_BLOG_SITE_ORIGIN",
        "SEO_BLOG_ADMIN_API_URL (optional for self-hosting)"
      ],
      "bestFor": "Sites with versioned content, a reproducible build, and deployment triggered by repository changes.",
      "authentication": "A tenant-scoped publishing token stored as a CI secret. It is never written into the repository configuration or generated content.",
      "publishingFlow": [
        "Create a project config and scheduled or manual CI workflow.",
        "Fetch only approved, due posts for the configured site.",
        "Write deterministic Markdown or MDX with project-specific frontmatter.",
        "Let the normal build deploy and verify the final public route."
      ],
      "fields": [
        "Content directory and file extension are explicit",
        "Frontmatter can match the framework schema",
        "Generated image data can be materialized under public assets"
      ],
      "canonicalOwnership": "The host framework exclusively owns routes, canonical tags, and the sitemap. The integration does not create a second hosted blog or a client-side content dependency.",
      "mediaHandling": "Embedded generated images can be written to deterministic asset files. Existing public image URLs can remain when their source and lifetime are suitable.",
      "limitations": [
        "The project must render the generated frontmatter and MDX syntax.",
        "A Git commit is not proof of a successful deploy.",
        "Build and sitemap behavior remain project-specific by design."
      ],
      "recovery": [
        "Review generated config and workflow in a pull request first.",
        "Test the publisher with the same Node version as CI.",
        "Fix generated content after a build failure and never print the publishing token."
      ]
    },
    {
      "id": "contentful",
      "name": "Contentful",
      "provider": "contentful",
      "adapter": "contentful",
      "aliases": [],
      "type": "cms",
      "status": "stable",
      "recommendedWave": 3,
      "docsUrl": "https://seodrafts.com/integrations/contentful/",
      "setupKind": "preview",
      "setupCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nCONTENTFUL_MANAGEMENT_TOKEN='<contentful-management-token>' \\\nCONTENTFUL_SPACE_ID='<contentful-space-id>' \\\nCONTENTFUL_ENVIRONMENT_ID='<contentful-environment-id>' \\\nCONTENTFUL_CONTENT_TYPE='<contentful-content-type>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli publish-cms --cms contentful --site-slug '<slug>' --site-origin '<origin>' --dry-run",
      "healthMode": "cms",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nCONTENTFUL_MANAGEMENT_TOKEN='<contentful-management-token>' \\\nCONTENTFUL_SPACE_ID='<contentful-space-id>' \\\nCONTENTFUL_ENVIRONMENT_ID='<contentful-environment-id>' \\\nCONTENTFUL_CONTENT_TYPE='<contentful-content-type>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nSEO_BLOG_SCHEDULED='true' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode cms --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "CONTENTFUL_MANAGEMENT_TOKEN",
        "CONTENTFUL_SPACE_ID",
        "CONTENTFUL_ENVIRONMENT_ID",
        "CONTENTFUL_CONTENT_TYPE",
        "SEO_BLOG_SITE_ORIGIN"
      ],
      "bestFor": "Headless websites that already render a Contentful content type and want reviewed SEO content to enter the same model and deployment path as editor-created entries.",
      "authentication": "A Contentful Management API token scoped to the destination space, plus the Space ID, environment, content type, and locale held in the server-side publisher.",
      "publishingFlow": [
        "Build a deterministic entry ID from the reviewed article slug.",
        "Map content fields and wrap values in the configured Contentful locale.",
        "Read the current entry version before creating or replacing the entry.",
        "Publish the saved version only in publish mode and retain the tenant-derived public URL."
      ],
      "fields": [
        "Field names can be overridden with an explicit field map",
        "Localized values use the configured CMS locale",
        "HTML body, excerpt, metadata, canonical, and social image are available"
      ],
      "canonicalOwnership": "The frontend consuming Contentful owns routing, canonical markup, rendering, sitemap output, and deployment. The adapter needs the production site origin because a Contentful entry ID is not a public page URL.",
      "mediaHandling": "The default map can pass an existing social image URL. Uploading binaries to Contentful Assets, processing them, and linking asset references remains a separate model-specific workflow.",
      "limitations": [
        "The configured content type must contain compatible fields or an explicit field map.",
        "The management token needs access to the selected space and environment.",
        "Publishing an entry does not prove that the frontend deployment has completed."
      ],
      "recovery": [
        "Start in draft mode and inspect the localized entry fields.",
        "On version conflicts, reread the current entry instead of retrying with a stale version.",
        "Verify the frontend route and deployment before accepting the derived public URL."
      ]
    },
    {
      "id": "sanity",
      "name": "Sanity",
      "provider": "sanity",
      "adapter": "sanity",
      "aliases": [],
      "type": "cms",
      "status": "stable",
      "recommendedWave": 3,
      "docsUrl": "https://seodrafts.com/integrations/sanity/",
      "setupKind": "preview",
      "setupCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nSANITY_TOKEN='<sanity-token>' \\\nSANITY_PROJECT_ID='<sanity-project-id>' \\\nSANITY_DATASET='<sanity-dataset>' \\\nSANITY_DOCUMENT_TYPE='<sanity-document-type>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli publish-cms --cms sanity --site-slug '<slug>' --site-origin '<origin>' --dry-run",
      "healthMode": "cms",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nSANITY_TOKEN='<sanity-token>' \\\nSANITY_PROJECT_ID='<sanity-project-id>' \\\nSANITY_DATASET='<sanity-dataset>' \\\nSANITY_DOCUMENT_TYPE='<sanity-document-type>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nSEO_BLOG_SCHEDULED='true' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode cms --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "SANITY_TOKEN",
        "SANITY_PROJECT_ID",
        "SANITY_DATASET",
        "SANITY_DOCUMENT_TYPE",
        "SEO_BLOG_SITE_ORIGIN"
      ],
      "bestFor": "Sanity-backed marketing sites with a known document type and frontend query that can render the mapped article fields on a canonical route.",
      "authentication": "A Sanity write token plus Project ID and dataset. The server-side publisher can also set the API version and document type so the mutation targets the intended schema.",
      "publishingFlow": [
        "Derive a stable document ID from the article slug.",
        "Create fields from the default mapping and tenant overrides.",
        "Write the native slug object and namespaced provenance metadata.",
        "Submit a create-or-replace mutation to the configured dataset and retain the frontend URL."
      ],
      "fields": [
        "Both bodyHtml and bodyMarkdown are available to the schema",
        "Slug is written as a native Sanity slug object",
        "Document type, API version, and field map are configurable"
      ],
      "canonicalOwnership": "Sanity stores content, but the consuming frontend controls the page route, canonical tag, sitemap, structured data, and deployment. SEODrafts derives the final route from the configured website rather than a Studio URL.",
      "mediaHandling": "Existing image URLs can be mapped as text fields. A schema that expects Sanity image references needs a separate upload and asset-reference step before the document mutation.",
      "limitations": [
        "Portable Text blocks are not invented from HTML without a tenant mapping or transformation.",
        "Create-or-replace requires a token that can write the chosen dataset.",
        "A successful mutation does not confirm that a cached or statically built frontend is live."
      ],
      "recovery": [
        "Test the exact document type and field map against a non-production dataset first.",
        "Inspect schema validation errors in Sanity when a field type rejects the mapped value.",
        "Reuse the deterministic document ID after fixing a failure instead of creating another document."
      ]
    },
    {
      "id": "strapi",
      "name": "Strapi",
      "provider": "strapi",
      "adapter": "strapi",
      "aliases": [],
      "type": "cms",
      "status": "stable",
      "recommendedWave": 3,
      "docsUrl": "https://seodrafts.com/integrations/strapi/",
      "setupKind": "preview",
      "setupCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nSTRAPI_URL='<strapi-url>' \\\nSTRAPI_TOKEN='<strapi-token>' \\\nSTRAPI_COLLECTION='<strapi-collection>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli publish-cms --cms strapi --site-slug '<slug>' --site-origin '<origin>' --dry-run",
      "healthMode": "cms",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nSTRAPI_URL='<strapi-url>' \\\nSTRAPI_TOKEN='<strapi-token>' \\\nSTRAPI_COLLECTION='<strapi-collection>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nSEO_BLOG_SCHEDULED='true' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode cms --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "STRAPI_URL",
        "STRAPI_TOKEN",
        "STRAPI_COLLECTION",
        "SEO_BLOG_SITE_ORIGIN"
      ],
      "bestFor": "Self-hosted or managed Strapi projects with a defined article collection and a frontend that already owns the public blog route.",
      "authentication": "A Strapi API token and server URL stored outside the browser, plus the API collection name used by the target content type.",
      "publishingFlow": [
        "Query the collection for an exact matching slug.",
        "Map the reviewed body, excerpt, SEO fields, canonical, and image URL.",
        "Update the existing document ID or create a new collection entry.",
        "Set publication state only for a live run and return the tenant website route."
      ],
      "fields": [
        "Supports HTML and Markdown body fields",
        "Works with documentId or numeric id responses",
        "Custom Strapi schemas use an explicit field map"
      ],
      "canonicalOwnership": "The frontend connected to Strapi owns the final route, canonical, rendering, sitemap, caching, and deployment. The CMS API response is stored as publishing evidence but is not treated as a separate public copy.",
      "mediaHandling": "The standard adapter maps a durable image URL. Strapi Media Library uploads and relations depend on the tenant schema and should be handled by a separate upload step.",
      "limitations": [
        "Collection names and field types differ between Strapi projects.",
        "Draft-and-publish behavior depends on the content type configuration.",
        "The API token needs both lookup and create or update permission for the collection."
      ],
      "recovery": [
        "Use dry run to verify the collection, origin, and field mapping.",
        "Check whether the API returns documentId or id before diagnosing an update failure.",
        "Resolve schema or permission errors in Strapi and rerun against the same slug."
      ]
    },
    {
      "id": "ghost",
      "name": "Ghost",
      "provider": "ghost",
      "adapter": "ghost",
      "aliases": [],
      "type": "cms",
      "status": "stable",
      "recommendedWave": 2,
      "docsUrl": "https://seodrafts.com/integrations/ghost/",
      "setupKind": "preview",
      "setupCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nGHOST_ADMIN_URL='<ghost-admin-url>' \\\nGHOST_ADMIN_API_KEY='<ghost-admin-api-key>' \\\nGHOST_API_VERSION='<ghost-api-version>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli publish-cms --cms ghost --site-slug '<slug>' --site-origin '<origin>' --dry-run",
      "healthMode": "cms",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nGHOST_ADMIN_URL='<ghost-admin-url>' \\\nGHOST_ADMIN_API_KEY='<ghost-admin-api-key>' \\\nGHOST_API_VERSION='<ghost-api-version>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nSEO_BLOG_SCHEDULED='true' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode cms --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "GHOST_ADMIN_URL",
        "GHOST_ADMIN_API_KEY",
        "GHOST_API_VERSION",
        "SEO_BLOG_SITE_ORIGIN"
      ],
      "bestFor": "Founder blogs, publications, and SaaS sites whose canonical content already lives in Ghost and should keep using the active Ghost theme and membership stack.",
      "authentication": "A Ghost Admin API key in id:secret format and the Admin URL. A five-minute JWT is created inside the server-side publishing process for each run.",
      "publishingFlow": [
        "Create a short-lived signed token for the Ghost Admin API.",
        "Look up the existing post using its exact slug and request HTML format.",
        "Create a new post or update the existing revision with reviewed fields.",
        "Use Ghost's returned post URL as the final publishing result."
      ],
      "fields": [
        "HTML is submitted through Ghost's source=html flow",
        "Meta title, meta description, tags, and feature image are mapped",
        "Existing posts include updated_at for safe revision updates"
      ],
      "canonicalOwnership": "Ghost and the active theme own the post route, canonical tag, sitemap, structured data, rendering, and public URL. The adapter records the URL returned by Ghost after saving.",
      "mediaHandling": "A durable public feature-image URL can be assigned to the post. Uploading a local file through the Ghost images endpoint requires a separate multipart media step.",
      "limitations": [
        "The Admin API key must be the id:secret value, not a Content API key.",
        "Custom theme fields outside the post model need a tenant-specific extension.",
        "Ghost API version changes should be tested before changing the configured Accept-Version."
      ],
      "recovery": [
        "Use draft mode and inspect the Ghost preview before first publication.",
        "For authentication failures, verify key format, server time, Admin URL, and API version.",
        "On revision conflicts, refetch the post so the next update carries its latest updated_at value."
      ]
    },
    {
      "id": "hubspot",
      "name": "HubSpot",
      "provider": "hubspot",
      "adapter": "hubspot",
      "aliases": [],
      "type": "cms",
      "status": "stable",
      "recommendedWave": 3,
      "docsUrl": "https://seodrafts.com/integrations/hubspot/",
      "setupKind": "preview",
      "setupCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nHUBSPOT_PRIVATE_APP_TOKEN='<hubspot-private-app-token>' \\\nHUBSPOT_CONTENT_GROUP_ID='<hubspot-content-group-id>' \\\nHUBSPOT_AUTHOR_ID='<hubspot-author-id>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli publish-cms --cms hubspot --site-slug '<slug>' --site-origin '<origin>' --dry-run",
      "healthMode": "cms",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nHUBSPOT_PRIVATE_APP_TOKEN='<hubspot-private-app-token>' \\\nHUBSPOT_CONTENT_GROUP_ID='<hubspot-content-group-id>' \\\nHUBSPOT_AUTHOR_ID='<hubspot-author-id>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nSEO_BLOG_SCHEDULED='true' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode cms --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "HUBSPOT_PRIVATE_APP_TOKEN",
        "HUBSPOT_CONTENT_GROUP_ID",
        "HUBSPOT_AUTHOR_ID",
        "SEO_BLOG_SITE_ORIGIN"
      ],
      "bestFor": "B2B marketing teams whose website, forms, campaigns, and canonical blog already run through HubSpot CMS Hub.",
      "authentication": "A HubSpot private app token with CMS blog access, plus the destination Content Group ID and Blog Author ID held in the publisher environment.",
      "publishingFlow": [
        "Search HubSpot blog posts for the reviewed slug.",
        "Map the title, body, summary, metadata, author, and blog group.",
        "Patch the existing post or create a native CMS blog post.",
        "Move the saved post to PUBLISHED only for a live run and record its returned URL."
      ],
      "fields": [
        "Content group and author are explicit required mappings",
        "Meta description, HTML title, body, and summary use native blog fields",
        "Featured image and alt text are sent when a durable URL exists"
      ],
      "canonicalOwnership": "HubSpot CMS owns the domain connection, URL structure, canonical markup, sitemap, templates, analytics, and public rendering. SEODrafts accepts the post URL returned by HubSpot as authoritative.",
      "mediaHandling": "The adapter can assign a public featured image URL. Uploading a file into HubSpot Files and selecting folder or access settings remains a separate tenant workflow.",
      "limitations": [
        "The private app needs the correct CMS scopes for reading and writing blog posts.",
        "Content Group and Author IDs must belong to the destination account.",
        "HubSpot theme modules beyond standard blog fields require custom template or field work."
      ],
      "recovery": [
        "Create a draft in the target blog and verify its author, template, and preview URL.",
        "Inspect private-app scopes and account IDs when HubSpot returns authorization errors.",
        "Rerun with the same slug after fixing the mapping so the adapter updates instead of duplicates."
      ]
    },
    {
      "id": "shopify",
      "name": "Shopify",
      "provider": "shopify",
      "adapter": "shopify",
      "aliases": [],
      "type": "cms",
      "status": "stable",
      "recommendedWave": 2,
      "docsUrl": "https://seodrafts.com/integrations/shopify/",
      "setupKind": "preview",
      "setupCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nSHOPIFY_ADMIN_TOKEN='<shopify-admin-token>' \\\nSHOPIFY_SHOP='<shopify-shop>' \\\nSHOPIFY_BLOG_ID='<shopify-blog-id>' \\\nSHOPIFY_API_VERSION='<shopify-api-version>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli publish-cms --cms shopify --site-slug '<slug>' --site-origin '<origin>' --dry-run",
      "healthMode": "cms",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nSHOPIFY_ADMIN_TOKEN='<shopify-admin-token>' \\\nSHOPIFY_SHOP='<shopify-shop>' \\\nSHOPIFY_BLOG_ID='<shopify-blog-id>' \\\nSHOPIFY_API_VERSION='<shopify-api-version>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nSEO_BLOG_SCHEDULED='true' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode cms --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "SHOPIFY_ADMIN_TOKEN",
        "SHOPIFY_SHOP",
        "SHOPIFY_BLOG_ID",
        "SHOPIFY_API_VERSION",
        "SEO_BLOG_SITE_ORIGIN"
      ],
      "bestFor": "Commerce brands that want informational or educational SEO content to live beside their storefront on the existing Shopify domain and theme.",
      "authentication": "A Shopify Admin API access token, shop hostname, Blog ID, and tested API version stored in the server-side publisher.",
      "publishingFlow": [
        "Query articles by handle and confirm the configured parent Blog ID.",
        "Build a native article input with HTML body, summary, tags, author, and image.",
        "Run articleCreate or articleUpdate based on the exact existing match.",
        "Reject GraphQL user errors and retain the storefront-derived canonical URL."
      ],
      "fields": [
        "Article handle comes from the reviewed slug",
        "Tags and author use native Shopify article fields",
        "A featured image URL is mapped with descriptive alt text"
      ],
      "canonicalOwnership": "The Shopify online store, blog route, theme, sitemap, canonical tags, and storefront deployment remain authoritative. The publisher writes to the selected native blog rather than creating a second hosted article.",
      "mediaHandling": "The GraphQL article input can reference a public image URL. Files that are private, local, or short-lived need to be uploaded to a durable location before the article mutation.",
      "limitations": [
        "The Admin API token needs article and blog access for the selected shop.",
        "Blog IDs are GraphQL global IDs and must match the article destination.",
        "Theme layout and commerce-specific components are not changed by the article publisher."
      ],
      "recovery": [
        "Dry-run with the production shop hostname and Blog ID before writing.",
        "Read GraphQL userErrors before retrying a rejected mutation.",
        "Confirm the handle and parent blog when an existing article is not detected."
      ]
    },
    {
      "id": "custom-webhook",
      "name": "Custom webhook",
      "provider": "webhook",
      "adapter": "webhook",
      "aliases": [
        "Custom CMS",
        "internal publisher"
      ],
      "type": "webhook",
      "status": "stable",
      "recommendedWave": 3,
      "docsUrl": "https://seodrafts.com/integrations/custom-webhook/",
      "setupKind": "preview",
      "setupCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nWEBHOOK_URL='<webhook-url>' \\\nWEBHOOK_TOKEN='<webhook-token>' \\\nWEBHOOK_SECRET='<webhook-secret>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nnpx --yes @wotaso/seo-blog-admin-cli publish-cms --cms webhook --site-slug '<slug>' --site-origin '<origin>' --dry-run",
      "healthMode": "webhook",
      "healthCommand": "SEO_BLOG_PUBLISH_TOKEN='<tenant-token>' \\\nSEO_BLOG_SITE_SLUG='<slug>' \\\nWEBHOOK_URL='<webhook-url>' \\\nWEBHOOK_TOKEN='<webhook-token>' \\\nWEBHOOK_SECRET='<webhook-secret>' \\\nSEO_BLOG_SITE_ORIGIN='<origin>' \\\nSEO_BLOG_SCHEDULED='true' \\\nnpx --yes @wotaso/seo-blog-admin-cli health --config seo-blog.config.json --mode webhook --site-slug '<slug>' --site-origin '<origin>'",
      "environmentVariables": [
        "SEO_BLOG_PUBLISH_TOKEN",
        "SEO_BLOG_SITE_SLUG",
        "WEBHOOK_URL",
        "WEBHOOK_TOKEN",
        "WEBHOOK_SECRET",
        "SEO_BLOG_SITE_ORIGIN"
      ],
      "bestFor": "Teams with an existing server-side publishing endpoint that can authenticate requests, validate a stable payload, upsert by post ID or slug, and return a public URL.",
      "authentication": "An HTTPS webhook URL with an optional bearer token and optional shared HMAC secret. All values stay in CI or another server-side secret environment.",
      "publishingFlow": [
        "Build normalized Markdown, HTML, metadata, assets, canonical, and source-post fields.",
        "Serialize one deterministic JSON request body and optionally sign it with HMAC-SHA256.",
        "POST the payload to the tenant-controlled HTTPS endpoint.",
        "Accept the returned id and publishedUrl only after a successful response."
      ],
      "fields": [
        "Payload includes the original approved post and normalized content",
        "The status field distinguishes draft and publish intent",
        "Response may return id or postId and publishedUrl or url"
      ],
      "canonicalOwnership": "The tenant endpoint is responsible for durable storage, idempotent updates, rendering, canonical markup, sitemap changes, deployment, and URL verification. SEODrafts records the URL it returns.",
      "mediaHandling": "The payload contains normalized visual-asset metadata and durable URLs when available. The receiving service decides whether to copy, transform, upload, or reject each asset.",
      "limitations": [
        "The receiving endpoint and its data contract are operated by the tenant.",
        "A 2xx response without a durable URL is not proof that a public page is live.",
        "Retries require an idempotent upsert keyed by the stable post ID or slug."
      ],
      "recovery": [
        "Validate the payload and signature against a staging endpoint first.",
        "Log request IDs and validation errors without logging tokens or shared secrets.",
        "Return a deterministic publishedUrl after the destination has accepted the upsert."
      ]
    },
    {
      "id": "squarespace",
      "name": "Squarespace reviewed handoff",
      "provider": "squarespace",
      "adapter": "manual_handoff",
      "aliases": [],
      "type": "manual",
      "status": "reviewed_fallback",
      "recommendedWave": 3,
      "docsUrl": "https://seodrafts.com/integrations/squarespace/",
      "setupKind": "manual",
      "setupCommand": null,
      "healthMode": null,
      "healthCommand": null,
      "environmentVariables": [],
      "bestFor": "Existing Squarespace sites that must keep their current templates and domain and can accept a reviewed HTML or Markdown handoff instead of direct native API publishing.",
      "authentication": "No Squarespace account password enters SEODrafts or an agent. The human editor uses the tenant's normal Squarespace session; the reviewed handoff itself needs no platform credential.",
      "publishingFlow": [
        "Prepare the approved body, metadata, assets, and intended route as a reviewed handoff.",
        "Give the reviewed package to a human editor without automating a Squarespace login.",
        "Create or update a Squarespace draft inside the tenant's normal editing workflow.",
        "Confirm the final public URL only after the canonical page has been checked live."
      ],
      "fields": [
        "The approved draft's reviewed HTML or Markdown can be copied into the handoff",
        "Metadata and intended slug stay attached to the handoff",
        "No browser password automation or fake native API is used"
      ],
      "canonicalOwnership": "Squarespace remains responsible for the public route, template, canonical tag, sitemap, analytics, and rendering. SEODrafts records the final Squarespace URL only after explicit confirmation.",
      "mediaHandling": "Use durable asset URLs or upload media through the tenant's normal Squarespace editing workflow. The fallback does not pretend that arbitrary image URLs became native platform assets.",
      "limitations": [
        "This is not a native Squarespace blog publishing connector or export CLI.",
        "A manual reviewed handoff is required before the page can go live.",
        "The destination URL and rendered metadata must be checked after entry."
      ],
      "recovery": [
        "Keep the item in review when the handoff cannot confirm a live URL.",
        "Fix formatting or asset issues in a draft before publishing.",
        "Use the same stable slug when retrying so the workflow does not create duplicate pages."
      ]
    }
  ]
}
