npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wotaso/seo-blog-admin-sdk

v0.2.2

Published

TypeScript SDK for secure SEO Drafts agent and publisher integrations.

Readme

SEO Blog Admin SDK

The versioned Agent API is the primary integration surface for assistants, CI agents, and server-side editorial automation. It uses scoped, expiring sda_live_... tokens and never accepts a mobile session or publisher token.

Agent API

Create an agent token in the SEODrafts dashboard with only the projects and scopes the integration needs. Keep it in a server-side secret store; never expose it to browser code, mobile binaries, prompts, logs, or command-line arguments.

import {
  SEO_BLOG_EDITORIAL_RESPONSIBILITY_VERSION,
  SeoBlogAgentClient,
  SeoBlogAgentError,
} from '@wotaso/seo-blog-admin-sdk';

const agent = new SeoBlogAgentClient({
  apiUrl: 'https://api.seodrafts.com',
  agentToken: process.env.SEODRAFTS_AGENT_TOKEN!,
});

const info = await agent.getInfo();
console.log(info.data.boundaries);

const firstPage = await agent.listPosts({
  siteSlug: 'your-project',
  status: 'pending_review',
  limit: 30,
});

for (const post of firstPage.data) {
  console.log(post.id, post.title, post.contentVersion, post.stateVersion);
}

if (firstPage.meta.hasMore && firstPage.meta.nextCursor) {
  const nextPage = await agent.listPosts({
    siteSlug: 'your-project',
    status: 'pending_review',
    limit: 30,
    cursor: firstPage.meta.nextCursor,
  });
  console.log(nextPage.meta.requestId);
}

Every successful call returns { data, meta }. meta always includes apiVersion and requestId; list endpoints also return hasMore and nextCursor.

Provider-backed generateReviewDraft() calls use a separate bounded ten-minute timeout (generationTimeoutMs) because research and generation can legitimately exceed the normal 20-second request timeout. A timeout remains an ambiguous mutation result: inspect the opportunity and post list before deciding whether a manual retry is needed; never blind-retry it.

Changes to an existing post use optimistic concurrency. Load the current post first, then send its contentVersion and monotone stateVersion with every mutation or publication-health check:

const current = (await agent.getPost(
  '00000000-0000-4000-8000-000000000000',
  { bodyOffset: 0, bodyLimit: 60_000 },
)).data;

const updated = await agent.updatePost(current.id, {
  expectedContentVersion: current.contentVersion,
  expectedStateVersion: current.stateVersion,
  title: 'A more precise, evidence-backed title',
});

const approved = await agent.approvePost(updated.data.id, {
  expectedContentVersion: updated.data.contentVersion,
  expectedStateVersion: updated.data.stateVersion,
  editorialResponsibilityAccepted: true,
  editorialResponsibilityVersion: SEO_BLOG_EDITORIAL_RESPONSIBILITY_VERSION,
  claimsVerified: true,
  originalValueConfirmed: true,
  authorIdentityConfirmed: true,
});

const scheduled = await agent.schedulePost(approved.data.id, {
  expectedContentVersion: approved.data.contentVersion,
  expectedStateVersion: approved.data.stateVersion,
  scheduledAt: new Date(Date.now() + 60 * 60 * 1000).toISOString(),
});

await agent.checkPostPublishHealth(scheduled.data.id, {
  expectedContentVersion: scheduled.data.contentVersion,
  expectedStateVersion: scheduled.data.stateVersion,
});

getPost() always requests an explicit bounded Markdown range and defaults to 60,000 characters. Continue with nextBodyOffset until it is null. If a later chunk has a different contentVersion or stateVersion, discard the partial body and restart at offset zero.

A 409 means the project or post changed after it was loaded. Fetch the latest representation and ask the human reviewer to reassess it; do not silently overwrite or approve a stale version. Project-context writes likewise send expectedContextVersion and the complete contextSourceUrls list. Published posts are immutable through the Agent API. Mutation responses intentionally contain only compact state/version/health metadata; use getPost() when the body or complete editorial metadata is needed.

updatePost() is content-only and rejects scheduledAt. Use schedulePost() as a separate, explicitly scoped mutation; approval also rejects scheduling fields, clears any pre-existing automation schedule, and requires the approved version to be scheduled afterward.

The client covers the complete /api/v1/agent/v1 contract:

| Area | Typed methods | | --- | --- | | Capability discovery | getInfo() | | Projects | listProjects(), getProject(), updateProjectContext(), updateProjectImageStyle() | | Posts | listPosts(), createPost(), getPost(), updatePost(), schedulePost(), approvePost(), declinePost(), restorePostToReview(), checkPostPublishHealth(), listPostRevisions() | | Opportunities | listOpportunities(), getOpportunity(), createOpportunity(), generateReviewDraft() | | Reporting | getInsights(), listIntegrations(), getBilling(), listAuditEvents() |

Connection safety is enforced before any request:

  • production origins must use HTTPS; HTTP is accepted only for localhost, *.localhost, 127.0.0.1, or [::1]
  • the base URL must be an origin without credentials, path, query, or fragment
  • requests cannot leave /api/v1/agent/v1
  • redirects are rejected so bearer credentials cannot cross origins
  • timeout defaults to 20 seconds and is bounded to 1–120 seconds
  • response bodies default to 1.5 MB and are bounded to 1 KB–10 MB
  • create/update post calls reject MDX, JSX/raw HTML, expressions, and dangerous Markdown URI schemes before transmission

Failures throw SeoBlogAgentError with status, code, requestId, retryAfterMs, and retryable. The SDK performs no automatic retries. Only read failures from GET/HEAD are ever marked retryable; mutations remain non-retryable until the API documents an idempotency-key contract.

SeoBlogAdminClient remains an additive compatibility alias for SeoBlogAgentClient. New integrations should use the Agent name because it makes the scoped security boundary explicit.

Publisher API

Consumer projects use this SDK to publish approved blog posts without sharing their database with the SEO Blog Admin app. The SDK returns structured content so each app can render its own blog template server-side or at build time.

import { SeoBlogPublisherClient, buildFilePublishingPayload } from '@wotaso/seo-blog-admin-sdk';
import { createHash } from 'node:crypto';
import { writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';

const client = new SeoBlogPublisherClient({
  apiUrl: process.env.SEO_BLOG_API_URL!,
  siteSlug: 'flashes',
  publishToken: process.env.SEO_BLOG_PUBLISH_TOKEN!,
});

const duePosts = await client.listDuePosts({ limit: 10 });

for (const post of duePosts) {
  if (post.approvalEpoch == null) throw new Error('Due post is missing its approval epoch.');
  const payload = buildFilePublishingPayload(
    post,
    {
      contentDir: 'content/blog',
      basePath: '/blog',
      faqRenderMode: 'details',
      faqTheme: 'card',
      faqClassName: 'blog-faq',
    },
    'https://example.com'
  );
  await mkdir(dirname(payload.filePath), { recursive: true });
  await writeFile(payload.filePath, payload.content);
  if (payload.publishedUrl) {
    await client.markPublished({
      postId: post.id,
      publishedUrl: payload.publishedUrl,
      expectedContentVersion: post.contentVersion,
      expectedApprovalEpoch: post.approvalEpoch,
      checksum: createHash('sha256').update(payload.content).digest('hex'),
    });
  }
}

Publisher clients require an HTTPS origin, reject redirects, embedded URL credentials, base paths, invalid limits, and implausible publish tokens before making a request. Loopback HTTP is available for local development. A local Compose service name can be opted in explicitly with insecureDevelopmentHosts: ['api']; never use that exception for a production network. Requests time out after 20 seconds and responses are capped at 1.5 MB by default. Server-side integrations with different bounded requirements can set timeoutMs (1–120 seconds) and maxResponseBytes (1 KB–10 MB) in the constructor.

The consumer owns rendering, deploys, routes, and templates. The SEO Blog Admin app owns research, generation, review, scheduling, and learning.

Publishing Integration Recommendation

Use the SDK integration matrix to show tenants the right setup path before asking them to create files or connect credentials:

import { listPublishingIntegrations, recommendPublishingIntegration } from '@wotaso/seo-blog-admin-sdk';

const recommendation = recommendPublishingIntegration({
  framework: 'Next.js',
  hasGitRepository: true,
  canInstallGitHubApp: true,
});

console.log(recommendation.integration.key); // github_app_pr
console.log(recommendation.nextSteps);

const webflow = recommendPublishingIntegration({ platform: 'Webflow' });
console.log(webflow.integration.key); // webflow

Recommendation rules:

  • Git-backed frameworks should use the GitHub App setup PR path when available, with the static publisher as the production-ready fallback.
  • WordPress, Webflow, Contentful, Sanity, Strapi, Ghost, Shopify, HubSpot, and custom CMS sites can use the tenant-run publish-cms CLI.
  • Framer remains a beta/native CMS path until its server-side automation is stable enough for a supported adapter.
  • Headless CMS sites should write CMS entries and trigger host build hooks only when the frontend needs a rebuild.
  • Hosted proxy pages are marked not_recommended and should not be the default SEO path.

CMS publishing example:

npx --yes @wotaso/[email protected] publish-cms \
  --cms contentful \
  --site-slug flashes \
  --site-origin https://flashes.app \
  --content-type blogPost \
  --field-map 'body=bodyHtml,metaTitle=metaTitle,metaDescription=metaDescription'

Astro Publishing

Astro is the first supported first-party integration target. It writes YAML-frontmatter Markdown into src/content/blog by default:

import { SeoBlogPublisherClient, buildAstroPublishingPayload } from '@wotaso/seo-blog-admin-sdk';
import { createHash } from 'node:crypto';
import { mkdir, writeFile } from 'node:fs/promises';
import { dirname } from 'node:path';

const client = new SeoBlogPublisherClient({
  apiUrl: process.env.SEO_BLOG_API_URL!,
  siteSlug: process.env.SEO_BLOG_SITE_SLUG!,
  publishToken: process.env.SEO_BLOG_PUBLISH_TOKEN!,
});

for (const post of await client.listDuePosts({ limit: 10 })) {
  if (post.approvalEpoch == null) throw new Error('Due post is missing its approval epoch.');
  const payload = buildAstroPublishingPayload(post, process.env.SITE_ORIGIN);
  await mkdir(dirname(payload.filePath), { recursive: true });
  await writeFile(payload.filePath, payload.content);
  if (payload.publishedUrl) {
    await client.markPublished({
      postId: post.id,
      publishedUrl: payload.publishedUrl,
      expectedContentVersion: post.contentVersion,
      expectedApprovalEpoch: post.approvalEpoch,
      checksum: createHash('sha256').update(payload.content).digest('hex'),
    });
  }
}

Future integrations should be added as publishing adapters beside the file-based helpers, not by changing the review or generation flow.

Render Contract

Each post includes both Markdown and structured fields:

  • tldr, keyTakeaways, readingTimeMinutes
  • authorName, authorTitle, authorUrl, authorImageUrl, optional reviewer fields

Author fields are post snapshots. Dashboard workspace defaults and project overrides are resolved when a post is created, so later profile changes do not silently alter existing or published content.

  • faqItems for FAQ UI and FAQPage schema
  • citations for source-backed claims
  • visualAssets for a branded hero plus optional section-specific explanatory graphics. Supporting assets expose visualType, sectionHeading, sectionContext, keyPoints, accessible alt, and their exact Markdown placeholder.
  • aiCitationTargets for answer-engine query targets

Recommended template order:

  1. H1, excerpt, author, updated date, reading time.
  2. TL;DR block and key takeaways.
  3. Main article body with direct-answer sections.
  4. Evidence, citations, comparison/process visuals where useful.
  5. FAQ, related posts, CTA, author/reviewer box.

Use buildSeoBlogStructuredData({ post, canonicalUrl, breadcrumbs }) plus jsonLdScripts(...) to render Article, FAQPage, optional BreadcrumbList, and custom post schema as crawlable JSON-LD. markdownFrontmatter, buildFilePublishingPayload, and buildAstroPublishingPayload replace visual placeholders such as <!-- visual:decision-flow --> with accessible inline figures when a matching visualAssets manifest exists. They can also append FAQ output from faqItems as details, markdown, frontmatter, or none. Consumer apps that render posts manually can call injectVisualAssetsIntoMarkdown(post.bodyMarkdown, post.visualAssets) and render faqItems with their own component.

Do not treat every asset as another cover image. The hero is a branded editorial poster. A section asset should sit beside the H2 named by sectionHeading and explain only the relationship described by sectionContext and keyPoints. Essential comparisons, measurements, steps, and claims must remain available as text or semantic tables; generated pixels are supplementary.

FAQ rendering is intentionally unstyled. details mode emits semantic accordion markup with stable classes, for example seo-blog-faq, seo-blog-faq__item, seo-blog-faq__question, and a theme modifier such as seo-blog-faq--card. Override faqClassName, faqItemClassName, faqQuestionClassName, and faqAnswerClassName when a host project already has its own design system.

For sitemaps, merge host-owned static URLs with await client.listSitemapEntries() and pass the result through mergeSitemapEntries(...) before sitemapXml(...). For images, visualAssetImageUrl(asset) resolves url, imageUrl, publicUrl, assetUrl, and related snake_case fields. The CLI can rewrite API-generated data:image/... visuals to static/CDN files with --asset-dir and --asset-public-base-url before it writes Markdown.