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),
});
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;
}
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.'
};
}
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';
}
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>;
}
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
];
That's the benefit of designing around contracts instead of specifics.
Top comments (0)