Skip to content

Commit 9ab9055

Browse files
author
Yann VR
committed
feat: add content formatting utilities for Medium platform
- Add contentFormat.ts with MDX to Markdown conversion - Add hashtag extraction and tag processing for Medium - Update MediumPlatform to use new content formatting utilities - Support automatic tag extraction from content hashtags
1 parent ed435b2 commit 9ab9055

2 files changed

Lines changed: 201 additions & 3 deletions

File tree

src/platforms/MediumPlatform.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BasePlatform, PostAnalytics } from './BasePlatform';
22
import { SocialPost, PostingResult } from '../types';
3+
import { validateAndPrepareContentForMedium, processTagsForMedium } from '../utils/contentFormat';
34
import axios from 'axios';
45

56
export class MediumPlatform extends BasePlatform {
@@ -21,6 +22,12 @@ export class MediumPlatform extends BasePlatform {
2122

2223
const { integrationToken } = this.credentials;
2324

25+
// Validate and convert content format for Medium (must be Markdown)
26+
const preparedContent = await validateAndPrepareContentForMedium(content.content);
27+
28+
// Process tags for Medium (extract from content, validate limits)
29+
const processedTags = processTagsForMedium(content.tags, preparedContent);
30+
2431
// First get user info to get author ID
2532
const userResponse = await axios.get('https://api.medium.com/v1/me', {
2633
headers: {
@@ -35,11 +42,11 @@ export class MediumPlatform extends BasePlatform {
3542

3643
// Prepare post data for Medium
3744
const postData = {
38-
title: content.title || content.content.substring(0, 50) + (content.content.length > 50 ? '...' : ''),
45+
title: content.title || preparedContent.substring(0, 50) + (preparedContent.length > 50 ? '...' : ''),
3946
contentFormat: 'markdown',
40-
content: content.content,
47+
content: preparedContent,
4148
canonicalUrl: content.url,
42-
tags: content.tags || [],
49+
tags: processedTags,
4350
publishStatus: 'public'
4451
};
4552

src/utils/contentFormat.ts

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { compile } from '@mdx-js/mdx';
2+
import { remark } from 'remark';
3+
import remarkStringify from 'remark-stringify';
4+
5+
export enum ContentFormat {
6+
MARKDOWN = 'markdown',
7+
MDX = 'mdx',
8+
UNKNOWN = 'unknown'
9+
}
10+
11+
/**
12+
* Detect the content format based on syntax patterns
13+
*/
14+
export function detectContentFormat(content: string): ContentFormat {
15+
// Check for MDX indicators first (more specific)
16+
if (hasMdxIndicators(content)) {
17+
return ContentFormat.MDX;
18+
}
19+
20+
// Check for Markdown indicators
21+
if (hasMarkdownIndicators(content)) {
22+
return ContentFormat.MARKDOWN;
23+
}
24+
25+
return ContentFormat.UNKNOWN;
26+
}
27+
28+
/**
29+
* Check if content has MDX-specific syntax
30+
*/
31+
function hasMdxIndicators(content: string): boolean {
32+
// Look for JSX elements (opening tags)
33+
const jsxElementRegex = /<\w+[^>]*>/;
34+
if (jsxElementRegex.test(content)) {
35+
return true;
36+
}
37+
38+
// Look for import/export statements (common in MDX)
39+
const importRegex = /^\s*(import|export)\s+/m;
40+
if (importRegex.test(content)) {
41+
return true;
42+
}
43+
44+
// Look for JSX expressions {expression}
45+
const jsxExpressionRegex = /\{[^}]*\}/;
46+
if (jsxExpressionRegex.test(content)) {
47+
return true;
48+
}
49+
50+
return false;
51+
}
52+
53+
/**
54+
* Check if content has Markdown-specific syntax
55+
*/
56+
function hasMarkdownIndicators(content: string): boolean {
57+
// Look for common markdown elements
58+
const markdownPatterns = [
59+
/^#{1,6}\s/m, // Headers
60+
/\[.*?\]\(.*?\)/, // Links
61+
/\*\*.*?\*\*/, // Bold
62+
/\*.*?\*/, // Italic
63+
/`.*?`/, // Inline code
64+
/```[\s\S]*?```/, // Code blocks
65+
/^\s*[-*+]\s/m, // List items
66+
/^\s*\d+\.\s/m, // Numbered list items
67+
/^>\s/m, // Blockquotes
68+
/!\[.*?\]\(.*?\)/, // Images
69+
];
70+
71+
// Count how many markdown patterns are found
72+
let markdownMatches = 0;
73+
for (const pattern of markdownPatterns) {
74+
if (pattern.test(content)) {
75+
markdownMatches++;
76+
}
77+
}
78+
79+
// If we find at least 2 markdown patterns, consider it markdown
80+
return markdownMatches >= 2;
81+
}
82+
83+
/**
84+
* Convert MDX content to Markdown
85+
*/
86+
export async function convertMdxToMarkdown(content: string): Promise<string> {
87+
try {
88+
// Use MDX compiler to parse and convert to markdown
89+
const result = await compile(content, {
90+
outputFormat: 'md',
91+
remarkPlugins: [],
92+
rehypePlugins: []
93+
});
94+
95+
// Convert the compiled result back to string
96+
const markdownContent = String(result);
97+
98+
return markdownContent;
99+
} catch (error) {
100+
// Fallback: try to remove JSX elements manually
101+
console.warn('MDX compilation failed, attempting manual cleanup:', error);
102+
103+
// Remove JSX elements
104+
let cleaned = content.replace(/<[^>]+>/g, '');
105+
106+
// Remove import/export statements
107+
cleaned = cleaned.replace(/^\s*(import|export).*?$/gm, '');
108+
109+
// Remove empty lines that were left behind
110+
cleaned = cleaned.replace(/^\s*$/gm, '').trim();
111+
112+
return cleaned;
113+
}
114+
}
115+
116+
/**
117+
* Validate that content is in a Medium-compatible format
118+
* Throws an error if content is not Markdown or MDX
119+
*/
120+
export async function validateAndPrepareContentForMedium(content: string): Promise<string> {
121+
const format = detectContentFormat(content);
122+
123+
switch (format) {
124+
case ContentFormat.MARKDOWN:
125+
// Already in correct format
126+
return content;
127+
128+
case ContentFormat.MDX:
129+
// Convert MDX to Markdown
130+
console.log('🔄 Converting MDX content to Markdown for Medium...');
131+
const markdown = await convertMdxToMarkdown(content);
132+
console.log('✅ MDX conversion completed');
133+
return markdown;
134+
135+
case ContentFormat.UNKNOWN:
136+
default:
137+
throw new Error(
138+
`Medium platform requires content in Markdown format. ` +
139+
`Detected content format: ${format}. ` +
140+
`Please provide content in Markdown (.md) or MDX (.mdx) format.`
141+
);
142+
}
143+
}
144+
145+
/**
146+
* Extract hashtags from content
147+
*/
148+
export function extractHashtagsFromContent(content: string): string[] {
149+
// Match hashtags that are standalone or at the end of lines
150+
// This regex looks for # followed by word characters, allowing for hyphens, underscores, and spaces in multi-word hashtags
151+
const hashtagRegex = /#([a-zA-Z][a-zA-Z0-9\s_-]*?)(?=\s|$|[^a-zA-Z0-9\s_-])/g;
152+
153+
const hashtags: string[] = [];
154+
let match;
155+
156+
while ((match = hashtagRegex.exec(content)) !== null) {
157+
const tag = match[1].trim().toLowerCase().replace(/[\s_-]+/g, '-'); // Normalize spaces and underscores to hyphens
158+
if (tag.length > 0 && tag.length <= 25) { // Medium tag limit
159+
hashtags.push(tag);
160+
}
161+
}
162+
163+
return [...new Set(hashtags)]; // Remove duplicates
164+
}
165+
166+
/**
167+
* Process and validate tags for Medium
168+
* Medium allows up to 5 tags, each up to 25 characters
169+
*/
170+
export function processTagsForMedium(explicitTags: string[] = [], content: string): string[] {
171+
// Extract hashtags from content
172+
const extractedTags = extractHashtagsFromContent(content);
173+
174+
// Combine explicit tags with extracted tags
175+
const allTags = [...explicitTags, ...extractedTags];
176+
177+
// Normalize tags: lowercase, trim, remove duplicates
178+
const normalizedTags = allTags
179+
.map(tag => tag.toLowerCase().trim())
180+
.filter(tag => tag.length > 0 && tag.length <= 25)
181+
.filter((tag, index, arr) => arr.indexOf(tag) === index); // Remove duplicates
182+
183+
// Medium allows maximum 5 tags
184+
const finalTags = normalizedTags.slice(0, 5);
185+
186+
if (normalizedTags.length > 5) {
187+
console.warn(`⚠️ Medium allows maximum 5 tags. Using first 5: ${finalTags.join(', ')}`);
188+
}
189+
190+
return finalTags;
191+
}

0 commit comments

Comments
 (0)