DEV Community

Aniefon Umanah
Aniefon Umanah

Posted on

Adding WordPress to a Multi-Platform Publishing System

When you're building a content publishing API that already supports Twitter, LinkedIn, and Dev.to, adding WordPress feels like it should be straightforward. It's just another REST API, right?

Not quite.

The WordPress REST API Quirk

Unlike the other platforms, WordPress doesn't use OAuth. It uses HTTP Basic Authentication with application passwords. This means instead of managing tokens and refresh flows, you're dealing with a simpler but more permanent credential system.

Here's what the authentication looks like:

const authString = Buffer.from(`${username}:${applicationPassword}`).toString('base64');

const response = await fetch(`${siteUrl}/wp-json/wp/v2/posts`, {
  method: 'POST',
  headers: {
    'Authorization': `Basic ${authString}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(postData),
});
Enter fullscreen mode Exit fullscreen mode

No token exchange, no scopes, no refresh logic. Just base64 encode the credentials and send them. This is both simpler and riskier—if those credentials leak, someone has write access to your WordPress site until you revoke them.

The Markdown Problem

The other platforms in this system accept plain text or markdown. WordPress expects HTML. This created a choice: require HTML input for WordPress posts, or convert markdown on the fly.

I went with conversion:

private markdownToHtml(markdown: string): string {
  let html = markdown;

  // Headers
  html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
  html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
  html = html.replace(/^# (.+)$/gm, ''); // Title handled separately

  // Code blocks
  html = html.replace(/```
{% endraw %}
(\w+)?\n([\s\S]*?)
{% raw %}
```/g, (_, lang, code) => {
    return `<pre><code class="language-${lang || 'plaintext'}">${code.trim()}</code></pre>`;
  });

  // Inline code
  html = html.replace(/`([^`]+)`/g, '<code>$1</code>');

  // Bold and italic
  html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
  html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');

  // Paragraphs
  html = html.split('\n\n').map(p => `<p>${p.trim()}</p>`).join('\n');

  return html;
}
Enter fullscreen mode Exit fullscreen mode

This is basic—it doesn't handle nested lists, tables, or complex markdown features. For a production system, you'd use a proper parser like marked. But it covers the common case: headers, code blocks, inline code, and basic formatting.

Error Handling That Actually Helps

When WordPress authentication fails, the REST API returns a 401. But there are multiple reasons why: wrong password, wrong username, application passwords not enabled, or even the REST API being disabled entirely.

The error handling differentiates these:

if (response.status === 401) {
  return { 
    success: false, 
    error: 'WordPress authentication failed. Check your username and application password.' 
  };
}
if (response.status === 403) {
  return { 
    success: false, 
    error: 'WordPress access forbidden. Ensure the user has permission to create posts.' 
  };
}
if (response.status === 404) {
  return { 
    success: false, 
    error: 'WordPress REST API not found. Ensure REST API is enabled and URL is correct.' 
  };
}
Enter fullscreen mode Exit fullscreen mode

The 404 case is particularly important. Some WordPress sites disable the REST API for security reasons. Knowing that upfront saves debugging time.

The Title Extraction

WordPress posts require a separate title field. Other platforms either don't have titles (Twitter) or extract them from the content (Dev.to). This meant extracting the title from markdown:

private extractTitle(content: string, providedTitle?: string): string {
  if (providedTitle?.trim()) {
    return providedTitle.trim();
  }

  // Look for # Title format
  const lines = content.split('\n');
  if (lines.length > 0 && lines[0].startsWith('# ')) {
    return lines[0].substring(2).trim();
  }

  // Fallback: use first 60 chars
  const firstLine = lines[0]?.trim() || '';
  return firstLine.length > 60 
    ? firstLine.substring(0, 57) + '...' 
    : firstLine || 'Untitled Post';
}
Enter fullscreen mode Exit fullscreen mode

If the content starts with # Title, use that. Otherwise, take the first line. If that's too long, truncate it. Always have a fallback.

What This Reveals About Platform Abstraction

The IPublisher interface treats all platforms the same:

interface IPublisher {
  supports(platform: Platform): boolean;
  publish(request: PublishRequest): Promise<PublishResult>;
}
Enter fullscreen mode Exit fullscreen mode

But each implementation is wildly different. Twitter has character limits and no formatting. LinkedIn wants structured posts with hashtags. Dev.to needs front matter and tags. WordPress wants HTML and separate titles.

The abstraction holds because it's minimal. It doesn't try to hide platform differences—it just provides a common shape for the publishing flow. The complexity lives in each publisher service where it belongs.

Adding WordPress meant writing a new service that handles its specific quirks while conforming to the same interface. No changes to the core publishing logic. Just register the new publisher:

this.publishers = [
  twitterPublisher, 
  devtoPublisher, 
  linkedinPublisher, 
  wordpressPublisher
];
Enter fullscreen mode Exit fullscreen mode

That's the benefit of designing around contracts instead of specifics.

Top comments (0)