368 lines
10 KiB
Markdown
368 lines
10 KiB
Markdown
---
|
|
description: Convert any content source to Medium-ready format
|
|
argument-hint: <input>
|
|
---
|
|
|
|
Convert any content source to Medium-ready format. This command is **adaptive** - it works with any input format and blog structure.
|
|
|
|
**Usage:** `$ARGUMENTS`
|
|
|
|
## Phase 1: Universal Input Detection
|
|
|
|
**Parse Input:**
|
|
- Extract content input from `$ARGUMENTS`
|
|
- Examples:
|
|
- `2025-10-06-my-post` (slug)
|
|
- `path/to/article.md` (file path)
|
|
- `https://myblog.com/post` (URL)
|
|
|
|
**Detect and Load Content:**
|
|
|
|
**If input looks like a file path** (contains `/` or file extension):
|
|
- Use Read tool to load the file
|
|
- Detect format by extension:
|
|
- `.md` / `.mdx` → Parse markdown with frontmatter
|
|
- `.pdf` → Inform user PDF parsing is limited, suggest markdown
|
|
- `.html` → Extract main content, strip HTML tags
|
|
- `.txt` → Read as plain text
|
|
- `.json` → Parse and extract relevant fields
|
|
|
|
**If input looks like a URL** (starts with `http://` or `https://`):
|
|
- Use WebFetch tool to retrieve the page
|
|
- Extract main article content, title, and description
|
|
- Parse and clean the text
|
|
|
|
**If input is a slug** (no `/` and no protocol):
|
|
- Search codebase using Glob: `**/*${input}*.md`
|
|
- Common blog locations:
|
|
- `src/content/blog/posts/**/*${input}*.md`
|
|
- `content/blog/*${input}*.md`
|
|
- `posts/*${input}*.md`
|
|
- `blog/*${input}*.md`
|
|
- Use Read tool to parse found markdown file
|
|
|
|
**Discover Blog Structure (if slug or file path):**
|
|
Explore the codebase to understand:
|
|
- 📁 Where are markdown files stored?
|
|
- 📋 What frontmatter format is used?
|
|
- 🖼️ Where are images/diagrams stored?
|
|
- 🎨 How are images referenced? (relative paths, absolute URLs, picture elements?)
|
|
- 🔗 What's the blog post URL structure?
|
|
|
|
**For URLs:** Extract and use the content as-is, skip blog structure discovery.
|
|
|
|
## Phase 2: Create Conversion Script
|
|
|
|
Write a **custom TypeScript conversion script** that handles their specific structure.
|
|
|
|
### Required Outputs (Universal Medium Best Practices):
|
|
|
|
**1. Image Handling - Upload Markers (CRITICAL)**
|
|
```typescript
|
|
// Medium strips base64 and external URLs fail
|
|
// Solution: Add clear upload marker for FIRST image only
|
|
// IMPORTANT: Only include the FIRST image from the blog post
|
|
return `\n\n---\n\n**📊 [UPLOAD IMAGE HERE: ${altText}]**\n\n*File: \`${relPath}\`*\n\n---\n\n`;
|
|
```
|
|
|
|
**2. References Format - Paragraphs Not Lists**
|
|
```typescript
|
|
// Medium adds blank numbers in lists
|
|
// Solution: Format as individual paragraphs
|
|
references.forEach(ref => {
|
|
output += `\n\n**[${ref.number}]** ${ref.content}`;
|
|
});
|
|
```
|
|
|
|
**3. Footnotes - Inline Citations**
|
|
```typescript
|
|
// Convert [^1] to inline: [Author, Year, Source]
|
|
text.replace(/\[\^(\d+)\]/g, (match, num) => {
|
|
return ` [${footnotes[num]}]`;
|
|
});
|
|
```
|
|
|
|
**4. Preview HTML with One-Click Copy**
|
|
```typescript
|
|
// Create HTML with simple one-click copy functionality
|
|
const previewHTML = generateOneClickCopyHTML(content);
|
|
fs.writeFileSync(previewPath, previewHTML);
|
|
|
|
// Auto-open in browser
|
|
exec(`${openCommand} "${previewPath}"`);
|
|
|
|
// Also open Medium editor
|
|
exec(`${openCommand} "https://medium.com/new-story"`);
|
|
```
|
|
|
|
**5. Attribution Footer - Specific URL**
|
|
```typescript
|
|
// Link to SPECIFIC blog post, not homepage
|
|
const blogPostURL = `${baseURL}/blog/${slug}`;
|
|
html += `<p><em>Originally published at <a href="${blogPostURL}">${siteName}</a></em></p>`;
|
|
```
|
|
|
|
**6. Clean HTML Conversion**
|
|
```typescript
|
|
// Use marked.js with:
|
|
marked.setOptions({
|
|
gfm: true,
|
|
breaks: false,
|
|
headerIds: false,
|
|
mangle: false
|
|
});
|
|
```
|
|
|
|
### One-Click Copy HTML Template
|
|
|
|
Generate an HTML with beautiful, simple one-click copy functionality:
|
|
|
|
```html
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Medium Article - One Click Copy</title>
|
|
<style>
|
|
/* Modern gradient background */
|
|
body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
|
|
|
/* Big prominent copy button */
|
|
.copy-button {
|
|
background: #03a87c;
|
|
color: white;
|
|
padding: 15px 40px;
|
|
border-radius: 30px;
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
cursor: pointer;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.copy-button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(3,168,124,0.3);
|
|
}
|
|
|
|
/* Content box is clickable */
|
|
.content-box {
|
|
background: white;
|
|
padding: 40px;
|
|
border-radius: 12px;
|
|
cursor: pointer;
|
|
position: relative;
|
|
}
|
|
|
|
.content-box::before {
|
|
content: "Click anywhere to copy";
|
|
position: absolute;
|
|
top: 10px;
|
|
right: 10px;
|
|
background: #f0f0f0;
|
|
padding: 5px 10px;
|
|
border-radius: 5px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
/* Success notification */
|
|
.status-message {
|
|
position: fixed;
|
|
top: 20px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: #4caf50;
|
|
color: white;
|
|
padding: 15px 30px;
|
|
border-radius: 30px;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.status-message.show { opacity: 1; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<button class="copy-button" onclick="copyContent()">
|
|
📋 Copy Article to Clipboard
|
|
</button>
|
|
<p>Or click anywhere in the content box below</p>
|
|
</div>
|
|
|
|
<div class="content-box" onclick="copyContent()">
|
|
<!-- Article content here -->
|
|
</div>
|
|
|
|
<div class="status-message" id="statusMessage">
|
|
✅ Content copied to clipboard!
|
|
</div>
|
|
|
|
<script>
|
|
function copyContent() {
|
|
const content = document.querySelector('.article-content').innerText;
|
|
navigator.clipboard.writeText(content).then(() => {
|
|
document.getElementById('statusMessage').classList.add('show');
|
|
setTimeout(() => {
|
|
document.getElementById('statusMessage').classList.remove('show');
|
|
}, 3000);
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Script Template Structure:
|
|
|
|
```typescript
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { marked } from 'marked';
|
|
|
|
const BLOG_BASE_URL = 'USER_PROVIDED_URL';
|
|
|
|
interface BlogMetadata {
|
|
title: string;
|
|
description: string;
|
|
// ... detected fields
|
|
}
|
|
|
|
function parseBlogPost(filePath: string) {
|
|
// Parse their specific frontmatter format
|
|
}
|
|
|
|
function convertImages(markdown: string, imagePath: string) {
|
|
// Handle THEIR image format
|
|
// Always output: upload markers
|
|
}
|
|
|
|
function formatReferences(markdown: string) {
|
|
// Extract references section
|
|
// Always output: paragraphs with [N] prefix
|
|
}
|
|
|
|
function convertToMedium(markdown: string, slug: string) {
|
|
// Apply all universal fixes
|
|
// Return clean HTML
|
|
}
|
|
|
|
function generatePreviewHTML(content: string, metadata: BlogMetadata) {
|
|
return `<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Medium Preview: ${metadata.title}</title>
|
|
<style>/* Clean, readable styling */</style>
|
|
</head>
|
|
<body>
|
|
<div class="instructions">
|
|
<h2>📋 How to Copy to Medium</h2>
|
|
<ol>
|
|
<li>Select all (Cmd/Ctrl+A)</li>
|
|
<li>Copy (Cmd/Ctrl+C)</li>
|
|
<li>Paste into Medium editor (Cmd/Ctrl+V)</li>
|
|
<li>Upload images at markers</li>
|
|
<li>Delete marker text after uploading</li>
|
|
<li>Publish!</li>
|
|
</ol>
|
|
</div>
|
|
<div id="content">${content}</div>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
async function main() {
|
|
// Parse blog post with their structure
|
|
// Convert with universal Medium fixes
|
|
// Generate preview and auto-open
|
|
}
|
|
```
|
|
|
|
## Phase 3: Execute & Guide
|
|
|
|
1. **Run the generated script** OR **Create HTML directly with Write tool**
|
|
- If using Write tool directly for preview HTML:
|
|
- **IMPORTANT**: Check if `medium-article-[LANG].html` exists first: `ls -la medium-article-[LANG].html 2>&1`
|
|
- If exists, use Read tool first (even just 1 line): `Read('medium-article-[LANG].html', limit=1)`
|
|
- Then use Write tool to create/update the file
|
|
2. **Verify preview opens** in browser
|
|
3. **Provide instructions:**
|
|
- How many images to upload
|
|
- Where each diagram file is located
|
|
- Copy-paste workflow
|
|
|
|
## Critical Success Factors
|
|
|
|
✅ **Image markers must be clear** - User needs exact file paths
|
|
✅ **References as paragraphs** - Avoid Medium's numbered list bugs
|
|
✅ **Preview auto-opens** - Streamlined workflow
|
|
✅ **Specific blog URL** - Not just homepage
|
|
✅ **Clean formatting** - No extra blank lines or artifacts
|
|
|
|
## Testing Checklist
|
|
|
|
After conversion, verify:
|
|
- [ ] Preview HTML opens automatically
|
|
- [ ] All images have upload markers with file paths
|
|
- [ ] References section has no blank numbers
|
|
- [ ] Footer links to specific blog post URL
|
|
- [ ] Footnotes converted to inline citations
|
|
- [ ] Code blocks preserved
|
|
- [ ] No HTML artifacts (picture tags removed, etc.)
|
|
|
|
## Example Interaction
|
|
|
|
```
|
|
User: /convert-to-medium
|
|
You: Which blog post would you like to convert? (provide path or slug)
|
|
User: src/posts/2024-01-15-my-post.md
|
|
You: What's your blog's base URL? (e.g., https://myblog.com)
|
|
User: https://myblog.com
|
|
|
|
[You explore their codebase]
|
|
|
|
You: I found:
|
|
- Markdown files in: src/posts/
|
|
- Images in: public/images/
|
|
- Frontmatter format: YAML with title, date, tags
|
|
- URL structure: /posts/{slug}
|
|
|
|
Creating conversion script...
|
|
|
|
[Generate and run custom script]
|
|
|
|
✅ Preview & Medium editor opened!
|
|
- Title: My Post
|
|
- 3 images to upload (markers added)
|
|
- References formatted as paragraphs
|
|
- Footer links to: https://myblog.com/posts/2024-01-15-my-post
|
|
|
|
SUPER SIMPLE Next Steps:
|
|
1. Click the BIG GREEN BUTTON (or click anywhere in content box) to copy
|
|
2. Switch to Medium tab and paste (Cmd/Ctrl+V)
|
|
3. Upload 3 images at the clearly marked spots
|
|
4. Publish!
|
|
```
|
|
|
|
## Key Differences from Hardcoded Script
|
|
|
|
❌ **Old way**: Hardcoded paths, specific to Kanaeru
|
|
✅ **New way**: Discovers structure, adapts to any blog
|
|
|
|
❌ **Old way**: One script for one blog
|
|
✅ **New way**: Generate custom script per blog
|
|
|
|
❌ **Old way**: User needs to modify code
|
|
✅ **New way**: User just provides blog post + URL
|
|
|
|
## Universal Best Practices (Always Apply)
|
|
|
|
These work for ANY blog, ANY structure:
|
|
|
|
1. **Images** → Upload markers (Medium limitation)
|
|
2. **References** → Paragraphs (Medium bug workaround)
|
|
3. **Footnotes** → Inline citations (Medium doesn't support footnotes)
|
|
4. **Preview** → Auto-open (UX improvement)
|
|
5. **Footer** → Specific URL (proper attribution)
|
|
6. **HTML** → Clean, minimal (Medium compatibility)
|
|
|
|
Be thorough in exploring their blog structure. Generate clean, working code. Test the output before declaring success.
|