SEODrafts

Home / Integration guide

Publish through the website you already own.

SEODrafts keeps research, drafting, approval, and learning together. The final page still belongs to your repository or CMS, on your production origin, with your navigation, canonical tag, sitemap, analytics, consent layer, and deployment.

Focused launch set

The public launch surface contains five paths only: GitHub App, Markdown/MDX, WordPress, Webflow, and Framer beta. Internal experimental adapters are not advertised or accepted by init-cms. We expand this list only after a connector has strict setup validation, idempotent updates, locale safety, recovery tests, and public deployment verification.

Recommended architecture

Use the host's strongest publishing surface

The best SEO result is not a separate Wotaso-hosted blog. The best result is a crawlable page on the tenant's own domain, using the platform that already controls templates, internal links, sitemap, analytics, and CDN delivery.

Git-backed frameworks

GitHub App PR, static publisher fallback

Best for Next.js, Astro, Nuxt, SvelteKit, Remix, Eleventy, Hugo, Gatsby, Docusaurus, VitePress, and Markdown/MDX blogs. The smooth SaaS version is a GitHub App that opens a setup PR. The production-ready path today is the static publisher.

  • Daily behavior: commit approved posts and assets, then let Vercel, Netlify, Coolify, or the host rebuild.
  • Sitemap: automatic when the sitemap reads the content directory; otherwise use the SDK server-side.
  • Secret: tenant publish token lives only in CI or server-side environment variables.

No-code CMS

CMS tenants need native CMS publishers

These tenants should not be asked to create GitHub Actions. Wotaso should map reviewed posts into the native CMS collection, publish the CMS item, and let the platform serve the canonical URL.

  • Webflow: create staged CMS items, publish item IDs, and publish the site when needed.
  • Framer: sync managed CMS collections; Server API is a beta path, so keep a plugin/export fallback.
  • Secret: OAuth grants or API credentials must be encrypted server-side.

CMS and headless CMS

Publish entries through tenant-run CLI adapters

WordPress, Webflow, Wix, Contentful, Sanity, Strapi, Ghost, Shopify, HubSpot, and custom CMS setups have tenant-run npx --yes @wotaso/seo-blog-admin-cli@0.2.2 publish-cms paths. Framer's Server API path is beta. Validate the selected tenant schema and the resulting public route before enabling scheduled writes; trigger a host build hook only for static frontends.

  • Direct CMS: WordPress, Ghost, Shopify, and HubSpot serve canonical posts after API publish.
  • Headless CMS: Contentful, Sanity, and Strapi field mapping must match the frontend schema and locale model.
  • Deploy hooks: use Vercel, Netlify, or Coolify hooks only after a CMS entry changes.

Avoid by default

Hosted proxy pages are a last resort

A Wotaso-hosted proxy or subdomain splits authority, internal links, analytics, conversion tracking, and UX from the tenant site. Use it only as a temporary demo when no canonical host integration is possible.

  • Preferred: tenant domain, tenant templates, tenant sitemap.
  • Fallback: authenticated webhook into a tenant-owned publisher.
  • Never: doorway pages, hidden content, fake reviews, or automated spam backlinks.

CNAME is possible, but not the default

A customer subdomain such as blog.example.com can point by CNAME to a hosted vendor and still be crawlable under the customer’s hostname. It is therefore not “worthless for SEO.” It does, however, create a separate host instead of example.com/blog/, adds DNS/TLS, cookie, consent, analytics, internal-link, outage, and migration responsibilities, and increases vendor lock-in. SEODrafts therefore publishes natively to the customer-owned site. A CNAME-hosted blog is a future fallback, not a launch connector and never a duplicate canonical copy.

Connector contracts

Stable · recommended

GitHub App

Install the App for the selected repository from the project dashboard. Review one setup pull request. Approved content then uses isolated content PRs and the repository’s existing checks and deployment.

  • Never writes below .github.
  • Never overwrites a tenant-owned file.
  • Marks a post published only after canonical and exact approval-version markers are live.

Run once in the website repository

npx --yes @wotaso/seo-blog-admin-cli@0.2.2 init-static \
  --site-slug flashes \
  --site-origin https://flashes.app \
  --content-dir apps/website/src/content/blog \
  --extension mdx

Then set the GitHub secret from the CLI

npx --yes @wotaso/seo-blog-admin-cli@0.2.2 login-password \
  --api-url https://api.seodrafts.com \
  --email owner@example.com \
  --password-stdin

npx --yes @wotaso/seo-blog-admin-cli@0.2.2 rotate-token flashes \
  --repo owner/repository

The CLI rotates a tenant-scoped publish token and writes it to the repository secret named SEO_BLOG_PUBLISH_TOKEN. Without --repo, it prints the token once for other CI or server-side secret stores.

Localization fails closed

Declare the CMS primary locale during setup. A translated WordPress or Webflow post is not allowed to fall through to the primary-locale slug. Framer beta is primary-locale-only and rejects translations. Add WordPress or Webflow mappings in seo-blog.config.json, for example:

{
  "cms": {
    "locale": "en-US",
    "localeMap": {
      "de-DE": "de-or-provider-locale-id"
    }
  }
}

Every localized page still needs its own canonical route, localized evidence and CTA, and the host template’s reviewed reciprocal hreflang output.

What “published” means

  1. The exact approved content version and approval epoch are still current.
  2. The destination accepts the item or repository change.
  3. The customer deployment finishes.
  4. The public URL returns HTML on the configured origin.
  5. The canonical matches the approved route.
  6. The live page contains the exact SEODrafts post/version marker; static publishing also checks the staged source fingerprint.
  7. Only then does the API change the article to published.

CMS CLI publisher

One command shape for nine stable CMS targets, plus Framer beta

Always begin with --dry-run. It reads approved due posts and builds a no-write payload and field-mapping preview, but it does not call the CMS provider, validate credentials or schema remotely, publish a page, or prove that a public canonical route exists. If no post is due, provider-specific preview work might not run. Remove --dry-run only after review; then verify the first rendered route before enabling the scheduled runner.

SEO_BLOG_PUBLISH_TOKEN=<tenant-token> \
SEO_BLOG_SITE_SLUG=flashes \
WEBFLOW_TOKEN=<webflow-token> \
WEBFLOW_COLLECTION_ID=<collection-id> \
npx --yes @wotaso/seo-blog-admin-cli@0.2.2 publish-cms \
  --cms webflow \
  --site-slug flashes \
  --site-origin https://flashes.app \
  --field-map 'post-body=bodyHtml,meta-title=metaTitle,meta-description=metaDescription' \
  --dry-run
WordPress: WORDPRESS_URL, WORDPRESS_USERNAME, WORDPRESS_APP_PASSWORD
Webflow: WEBFLOW_TOKEN, WEBFLOW_COLLECTION_ID, --field-map
Framer (beta): FRAMER_API_KEY, FRAMER_PROJECT_URL, FRAMER_COLLECTION
Wix: WIX_API_KEY, WIX_SITE_ID, WIX_MEMBER_ID
Contentful: CONTENTFUL_MANAGEMENT_TOKEN, CONTENTFUL_SPACE_ID, CONTENTFUL_CONTENT_TYPE
Sanity: SANITY_TOKEN, SANITY_PROJECT_ID, SANITY_DATASET
Strapi: STRAPI_URL, STRAPI_TOKEN, STRAPI_COLLECTION
Ghost: GHOST_ADMIN_URL, GHOST_ADMIN_API_KEY
Shopify: SHOPIFY_SHOP, SHOPIFY_ADMIN_TOKEN, SHOPIFY_BLOG_ID
HubSpot: HUBSPOT_PRIVATE_APP_TOKEN, HUBSPOT_CONTENT_GROUP_ID, HUBSPOT_AUTHOR_ID
Webhook: WEBHOOK_URL, optional WEBHOOK_SECRET

Squarespace uses a reviewed manual handoff

There is no native Squarespace publisher command in this flow. Hand the approved Markdown or HTML to a human editor, create or update the draft in the tenant's normal Squarespace session, and verify the rendered route, metadata, assets, and canonical URL before recording completion. Do not request a Squarespace password or show an optional Squarespace webhook command. If the tenant already owns an authenticated custom publisher, select the generic webhook connector separately and document it as that connector.

Scheduled-runner health heartbeat

Run this from the real CMS cron or server runner after seo-blog.config.json contains the matching site identity and cms.provider. Keep the publish token and provider credential in that runner's secret store.

{
  "apiUrl": "https://api.seodrafts.com",
  "siteSlug": "flashes",
  "siteOrigin": "https://flashes.app",
  "publishTokenEnv": "SEO_BLOG_PUBLISH_TOKEN",
  "cms": { "provider": "webflow" }
}
SEO_BLOG_PUBLISH_TOKEN=<tenant-token> \
SEO_BLOG_SCHEDULED=true \
WEBFLOW_TOKEN=<webflow-token> \
npx --yes @wotaso/seo-blog-admin-cli@0.2.2 health \
  --config seo-blog.config.json \
  --mode cms \
  --site-slug flashes \
  --site-origin https://flashes.app

The heartbeat checks the runner's config, site identity, declared schedule, publish token, and presence of the configured provider secret; it does not validate the provider credential remotely or prove that a page is live. Ready status is temporary: use the server-provided publisherHealthNextExpectedAt deadline, or treat a heartbeat older than 36 hours as stale when no deadline is available.

Before enabling the schedule

  1. Run the initializer with production-like credentials and the real primary locale.
  2. Review the generated config/workflow or GitHub setup PR.
  3. Confirm the content directory or CMS field map and public route.
  4. Run the read-only health command from the real scheduled environment.
  5. Publish one approved article manually and inspect mobile layout, images, headings, canonical, schema, sitemap, consent, and analytics coverage.
  6. Enable automation only after the public verification succeeds.