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
class PublisherManager {
constructor(content: string);
}
Parameters:
content
(string): Processed Markdown content from ArticleProcessor
Methods
addPlugin
Registers a publisher plugin for a specific platform.
addPlugin(plugin: PublisherPlugin): void
Parameters:
plugin
(PublisherPlugin): Platform-specific publisher plugin
Example:
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.
publish(): Promise<PublishResult[]>
Returns:
Promise<PublishResult[]>
: Array of results from each platform
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:
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 headingvisit
: AST visitor for platform-specific content transformationstoMarkdown
: Function to convert modified AST back to Markdown
Complete Example
Basic Multi-Platform Publishing
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
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:
First Publication:
- Generates unique article ID
- Stores platform-specific post IDs in
postMapRecords.json
Subsequent Updates:
- Retrieves existing post IDs
- Updates existing posts instead of creating duplicates
postMapRecords.json Structure
{
"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:
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:
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:
// All platforms publish simultaneously
const results = await publisher.publish();
Sequential Publishing
For rate-limited APIs or ordered dependencies:
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
Environment Variables: Store API keys and sensitive data in environment variables
bashNOTION_API_KEY=secret_xxx DEVTO_API_KEY=xxx
Error Recovery: Implement retry logic for transient failures
tsasync 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)); } }
Logging: Implement comprehensive logging for debugging
tspublisher.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; }
Validation: Validate content before publishing
tsif (!content || content.trim().length === 0) { throw new Error("Content is empty"); } if (!content.includes("# ")) { console.warn("No title found in content"); }