Skip to content

PublisherManager

PublisherManager is responsible for orchestrating the publication of processed articles to multiple platforms simultaneously. It manages publisher plugins and coordinates the publishing workflow.

Overview

The PublisherManager class:

  • Manages multiple publisher plugins
  • Publishes content to all registered platforms in parallel
  • Tracks published articles for future updates
  • Provides unified error handling and result reporting

Constructor

ts
class PublisherManager {
  constructor(content: string);
}

Parameters:

  • content (string): Processed Markdown content from ArticleProcessor

Methods

addPlugin

Registers a publisher plugin for a specific platform.

ts
addPlugin(plugin: PublisherPlugin): void

Parameters:

  • plugin (PublisherPlugin): Platform-specific publisher plugin

Example:

ts
const manager = new PublisherManager(content);
manager.addPlugin(NotionPublisherPlugin({ api_key: "...", page_id: "..." }));
manager.addPlugin(DevToPublisherPlugin({ api_key: "...", published: false }));

publish

Publishes the article to all registered platforms simultaneously.

ts
publish(): Promise<PublishResult[]>

Returns:

  • Promise<PublishResult[]>: Array of results from each platform
ts
interface PublishResult {
  pid?: string; // Platform-specific post ID
  name?: string; // Plugin name
  success: boolean; // Whether publication succeeded
  info?: string; // Success/error message
}

PublisherPlugin Interface

All publisher plugins must implement this interface:

ts
interface PublisherPlugin {
  extendsParam?(extendsParam: ExtendsParam): PublisherPlugin; // Receive tracked article data
  process(articleTitle: string, visit: TVisitor, toMarkdown: ToMarkdown): Promise<PublishResult>;
  update?(article_id: string | undefined, articleTitle: string, content: string): Promise<void>; // Update existing articles
  name: string; // Plugin identifier
  isTraceUpdate?: boolean; // Track for future updates
}

interface ExtendsParam {
  pid?: string; // Existing post ID from tracking
}

type ToMarkdown = () => { content: string };

Plugin Parameters

  • articleTitle: Extracted article title from the first H1 heading
  • visit: AST visitor for platform-specific content transformations
  • toMarkdown: Function to convert modified AST back to Markdown

Complete Example

Basic Multi-Platform Publishing

ts
import { ArticleProcessor, PublisherManager, NotionPublisherPlugin, DevToPublisherPlugin, NativePublisherPlugin } from "@artipub/core";
import path from "path";

// Step 1: Process the article
const processor = new ArticleProcessor({
  uploadImgOption: {
    owner: process.env.GITHUB_OWNER!,
    repo: process.env.GITHUB_REPO!,
    dir: "images",
    branch: "main",
    token: process.env.GITHUB_TOKEN!,
    commit_author: "Bot",
    commit_email: "bot@example.com",
  },
});

const { content } = await processor.processMarkdown(path.resolve(__dirname, "./articles/my-post.md"));

// Step 2: Setup publisher
const publisher = new PublisherManager(content);

// Add Notion publisher
publisher.addPlugin(
  NotionPublisherPlugin({
    api_key: process.env.NOTION_API_KEY!,
    page_id: process.env.NOTION_PAGE_ID!,
  })
);

// Add Dev.to publisher
publisher.addPlugin(
  DevToPublisherPlugin({
    api_key: process.env.DEVTO_API_KEY!,
    published: false, // Save as draft
    series: "My Tutorial Series",
    main_image: "https://example.com/cover.jpg",
    description: "Article description for SEO",
  })
);

// Add local file publisher
publisher.addPlugin(
  NativePublisherPlugin({
    destination_path: "/home/user/blog/content/posts",
    cdn_prefix: "https://cdn.jsdelivr.net/gh",
    res_domain: "raw.githubusercontent.com",
  })
);

// Step 3: Publish to all platforms
const results = await publisher.publish();

// Step 4: Handle results
results.forEach((result) => {
  if (result.success) {
    console.log(`✅ ${result.name}: ${result.info}`);
  } else {
    console.error(`❌ ${result.name}: ${result.info}`);
  }
});

Advanced: Custom Publisher Plugin

ts
import { PublisherPlugin, PublishResult } from "@artipub/core";
import axios from "axios";

// Create a custom publisher for your platform
function CustomBlogPublisher(options: CustomBlogOptions): PublisherPlugin {
  return {
    name: "CustomBlog",
    isTraceUpdate: true, // Enable update tracking

    extendsParam(params) {
      // Extend with existing post ID for updates
      if (params.pid) {
        options.postId = params.pid;
      }
    },

    async process(articleTitle, visit, toMarkdown) {
      try {
        // Transform content for your platform
        visit("image", (node) => {
          // Convert image URLs to your CDN
          node.url = `https://mycdn.com/proxy?url=${encodeURIComponent(node.url)}`;
        });

        // Remove elements not supported by your platform
        visit("html", (node, index, parent) => {
          parent.children.splice(index, 1);
        });

        // Get the transformed content
        const { content } = toMarkdown();

        // Publish to your platform
        const endpoint = options.postId ? `https://api.myblog.com/posts/${options.postId}` : "https://api.myblog.com/posts";

        const response = await axios({
          method: options.postId ? "PUT" : "POST",
          url: endpoint,
          headers: {
            Authorization: `Bearer ${options.apiKey}`,
            "Content-Type": "application/json",
          },
          data: {
            title: articleTitle,
            content: content,
            tags: options.tags,
            draft: options.draft,
          },
        });

        return {
          success: true,
          info: `Published to CustomBlog: ${response.data.url}`,
          pid: response.data.id, // Save for future updates
        };
      } catch (error) {
        return {
          success: false,
          info: `Failed to publish: ${error.message}`,
        };
      }
    },
  };
}

// Use the custom publisher
const publisher = new PublisherManager(content);
publisher.addPlugin(
  CustomBlogPublisher({
    apiKey: process.env.CUSTOM_BLOG_API_KEY!,
    tags: ["tutorial", "javascript"],
    draft: false,
  })
);

Article Update Tracking

ArtiPub automatically tracks published articles for updates when isTraceUpdate is enabled:

  1. First Publication:

    • Generates unique article ID
    • Stores platform-specific post IDs in postMapRecords.json
  2. Subsequent Updates:

    • Retrieves existing post IDs
    • Updates existing posts instead of creating duplicates

postMapRecords.json Structure

json
{
  "article_unique_id_123": {
    "NotionPublisher": {
      "k": "notion-page-id-456"
    },
    "DevToPublisher": {
      "k": "devto-article-id-789"
    }
  }
}

Error Handling

Individual Plugin Failures

By default, if one plugin fails, others continue publishing:

ts
const results = await publisher.publish();

const successful = results.filter((r) => r.success);
const failed = results.filter((r) => !r.success);

console.log(`Published to ${successful.length}/${results.length} platforms`);

if (failed.length > 0) {
  console.error(
    "Failed platforms:",
    failed.map((f) => f.name)
  );
}

Fail-Fast Strategy

To stop on first failure:

ts
class StrictPublisherManager extends PublisherManager {
  async publish() {
    const results = [];

    for (const plugin of this.plugins) {
      const result = await plugin.process(/* ... */);

      if (!result.success) {
        throw new Error(`Publishing failed: ${result.info}`);
      }

      results.push(result);
    }

    return results;
  }
}

Performance Optimization

Parallel Publishing

By default, PublisherManager publishes to all platforms in parallel for optimal performance:

ts
// All platforms publish simultaneously
const results = await publisher.publish();

Sequential Publishing

For rate-limited APIs or ordered dependencies:

ts
async function publishSequentially(publisher: PublisherManager) {
  const results = [];

  // Publish one by one
  for (const plugin of publisher.plugins) {
    const result = await plugin.process(/* ... */);
    results.push(result);

    // Add delay if needed
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }

  return results;
}

Best Practices

  1. Environment Variables: Store API keys and sensitive data in environment variables

    bash
    NOTION_API_KEY=secret_xxx
    DEVTO_API_KEY=xxx
  2. Error Recovery: Implement retry logic for transient failures

    ts
    async function publishWithRetry(publisher, maxRetries = 3) {
      for (let i = 0; i < maxRetries; i++) {
        const results = await publisher.publish();
        const failed = results.filter((r) => !r.success);
    
        if (failed.length === 0) return results;
    
        console.log(`Retry ${i + 1}/${maxRetries} for failed platforms...`);
        await new Promise((resolve) => setTimeout(resolve, 2000));
      }
    }
  3. Logging: Implement comprehensive logging for debugging

    ts
    publisher.addPlugin(withLogging(NotionPublisherPlugin(options), "Notion"));
    
    function withLogging(plugin, name) {
      const original = plugin.process;
      plugin.process = async (...args) => {
        console.log(`Publishing to ${name}...`);
        const start = Date.now();
    
        try {
          const result = await original(...args);
          console.log(`${name} completed in ${Date.now() - start}ms`);
          return result;
        } catch (error) {
          console.error(`${name} failed:`, error);
          throw error;
        }
      };
      return plugin;
    }
  4. Validation: Validate content before publishing

    ts
    if (!content || content.trim().length === 0) {
      throw new Error("Content is empty");
    }
    
    if (!content.includes("# ")) {
      console.warn("No title found in content");
    }

API Reference

Released under the MIT License.