Skip to content

Publish Adapters ​

A publish adapter creates and updates remote articles. It is separate from Export, which produces files, and Data Sync, which synchronizes storage. Use the Publish store tag; use both Export and Publish when an extension also offers conversion/export commands.

Manifest ​

json
{
  "id": "example.blog",
  "name": "Example Blog",
  "version": "1.0.0",
  "tags": ["Publish"],
  "main": "main.js",
  "permissions": {
    "document": "read",
    "network": ["{config.siteUrl}"]
  },
  "contributes": {
    "publishAdapters": [
      { "id": "blog", "title": "Example Blog", "capabilities": ["draft", "media"] }
    ],
    "configuration": {
      "properties": {
        "siteUrl": { "type": "string", "default": "", "title": "Site" }
      }
    }
  }
}

Adapter IDs are unique within an extension. The public key is example.blog:blog, used by typora publish --to. main and document read permission are required. capabilities is an explicit array drawn from draft, media, metrics, comments; an empty array is valid. It advertises what the implementation actually supports, not everything the website UI can do. Undeclared capabilities cannot be invoked by the shared host.

Folder destinations in Settings ▸ Export and Publish require a declared string siteUrl configuration property. The host overrides that property (and optional collection) only in the destination's snapshot Worker. The permission checker uses the same trusted destination settings, so a {config.siteUrl} grant remains limited to the selected site origin. Adapters using a different endpoint setting can still be invoked directly, but are not offered as available folder destination adapters.

Each platform is its own installable extension. No platform-specific switch belongs in the publish core. Settings and network requests follow the normal extension permission model. Disable/uninstall/revocation makes an adapter unavailable for new calls. No new raw filesystem or unrestricted network API is introduced.

Worker registration ​

js
export function activate(ctx) {
  ctx.subscriptions.push(
    typora.publish.registerAdapter("blog", async request => {
      switch (request.operation) {
        case "prepare": return prepare(request.source);
        case "get": return get(request.document, request.remote);
        case "create": return create(request.document, request.draft);
        case "update": return update(request.document, request.remote, request.draft);
        case "metrics": return metrics(request.document, request.remote);
        case "comments": return comments(request.document, request.remote);
      }
    })
  );
}

registerAdapter(id, handler) returns a disposable. Registration is repeated when an idle Worker restarts. The host activates main on demand and invokes publish.run { id, request }; onPublishAdapter:<id> is an accepted activation event. The handler must not access an unrelated active editor: requests contain an explicit source snapshot, and CLI runs use a dedicated snapshot Worker.

typora.publish.current(id, options?) runs the shared core on this window's saved-path document with its current live Markdown. Options: dryRun, draft, status, refresh, metrics, comments, bind, forget. The host supplies the extension and adapter key; an extension cannot use this API to publish with another extension's authority. The returned promise resolves to a PublishResult. A command should present the resulting action/error; a detail view can use status: true and forward the data using the existing view messaging API.

Requests and results, protocol v1 ​

OperationInputResult
preparesource: { path, markdown }PreparedPublication
getdocument, remoteRemotePublication or null for confirmed missing only
createdocument, optional draftRemotePublication, after verifying the saved article
updatedocument, remote, optional draftRemotePublication, preserving identity
metricsdocument, remotePlatform data with explicit availability and fetch time
commentsdocument, remotePlatform comments, with pagination/truncation disclosed

PreparedPublication contains:

ts
interface PreparedPublication {
  account: string;       // non-secret site/user/collection identity
  documentId?: string;   // explicit publish_id; otherwise host uses source.path
  title: string;
  markdown: string;      // body without front matter
  metadata: Record<string, unknown>;
  fingerprint: string;   // source + relevant settings + image byte hashes
  [key: string]: unknown; // adapter-private structured-cloneable payload
}

The host maps top-level status: draft to the draft request and checks the adapter capability before any remote mutation. Other status values are platform-specific.

prepare must not perform remote writes, upload images, ask for credentials, or change document text. It may read referenced local images through typora.document.readImage, compute hashes and inspect settings. Parse YAML without evaluation; reject unsupported values rather than silently publishing wrong metadata. A credential belongs in private extension storage, never in account, fingerprint, results, logs or source front matter. The bundled extensions currently use a limited top-level front matter parser: scalar values and lists; nested YAML, anchors and multiline scalar metadata are not supported. Body Markdown remains intact.

RemotePublication contains:

ts
interface RemotePublication {
  id: string;            // opaque, escaped when placed in a request URL
  state: "draft" | "published" | "scheduled" | "private" | "pending" | "unknown";
  url?: string;          // published article URL
  editUrl?: string;      // authenticated editor/draft URL
  revision: string;      // deterministic content/version token
}

The revision must change when remotely editable content, relevant metadata or publication state changes. Views, comments and other engagement must not change it. Read after writing to normalize server-generated content before returning the baseline. A readback failure after a write is an uncertain outcome; do not create again automatically. APIs lacking conditional writes provide best-effort conflict detection only.

PublishResult has source, sync, optional action (created, updated, skipped), remote, checkedAt, optional metrics, comments, error. Remote state and local synchronization are independent. The host returns { supported: false } for undeclared metrics/comments. An adapter must never fabricate zero counts or a public URL for a private draft.

Contributor responsibilities ​

  • Declare exact network origins, including a central API origin when it differs from the user's blog URL. Reuse the normal permission-checked fetch API.
  • Take credentials in a format: "secret" option and keep them with typora.secrets, never in typora.storage. Provide a Sign Out command. The CLI must fail with an actionable sign-in error, not open an interactive prompt. Explain what you keep and how to revoke it in the README.
  • Preserve remote identity and distinguish 404 from 401/403/network errors. Never turn a failed read into an automatic create.
  • Reject unsupported drafts/media before article mutation. Upload images by content hash per account. Do not replace the source document's local links.
  • Do not retry non-idempotent requests after timeouts. Observe HTTP 429 and expose the failure; the user can retry after checking uncertain writes.
  • Return comment data as untrusted content. Include a cursor or indicate a truncated response. Statistics must explain their meaning and timestamp.
  • Keep optional capabilities independent. Do not require analytics credentials merely to publish an article.
  • Add mocked HTTP tests for create/update/get, authentication failure, missing articles, state mapping, media uploads and unsupported operations. A live test blog is needed to verify API-specific deployment behavior.
  • Bump the version on every existing extension package change. Run store validation/build and the publish core tests before submitting the PR.

The host owns fingerprints, bindings, atomic local records, batch behavior and conflict decisions. Extension code owns authentication, platform content conversion, remote reads/writes and optional platform data. See the repository's docs/publish.md for complete CLI/state/recovery behavior.

Theme adapted from Nimbus (MIT).