- name
- convert-newsletter
- description
- Converts Substack (or Beehiiv) newsletter posts into properly formatted blog markdown files. Use when converting newsletters to blog posts, importing Substack/Beehiiv content, or creating blog posts from newsletter URLs.
- allowed-tools
- ["Read","Write","Bash","Glob","Grep","mcp__fetch__imageFetch","WebFetch"]
# Converting Newsletter to Blog Post
A comprehensive guide for converting Substack (or Beehiiv) newsletter posts into properly formatted blog markdown files for the website.
## Supported Platforms
- **Substack** (primary): `https://didierrlopes.substack.com/p/<slug>` or `https://substack.com/home/post/p-<id>`
- **Beehiiv** (legacy): `https://didierlopes.beehiiv.com/p/<slug>`
## Prerequisites
- Access to the newsletter URL
- Image extraction capabilities (use mcp__fetch__imageFetch tool)
- Understanding of the blog's markdown structure and front matter requirements
## Step-by-Step Conversion Process
### 1. Extract Newsletter Content
Use the imageFetch tool to extract content and images:
```
mcp__fetch__imageFetch with url=<newsletter-url> and images={"output": "file", "layout": "individual", "maxCount": 10}
```
**Platform-specific notes:**
**Substack:**
- Content is usually well-extracted via imageFetch in markdown mode
- For posts accessed via `substack.com/home/post/p-<id>`, the content may be embedded in JSON within the HTML. Use `raw=true` and extract the `body_html` field from the embedded JSON
- Clean Substack tracking params: remove `?utm_source=didierlopes.beehiiv.com&utm_medium=newsletter&utm_campaign=...` from URLs
- Substack image URLs follow the pattern: `https://substackcdn.com/image/fetch/.../https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F<uuid>_<dimensions>.<ext>`
- To download high-res images, construct URL: `https://substackcdn.com/image/fetch/w_1200,c_limit,f_png,q_auto:good/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F<uuid>_<dimensions>.<ext>`
**Beehiiv:**
- Content is directly extractable via imageFetch
- Beehiiv CDN image URLs may expire, so download immediately
**Key extraction requirements:**
- Capture all text content including headings, paragraphs, and lists
- Extract all images in high quality (minimum 1000px width)
- Preserve link URLs and their context
- Note any special formatting (bold, italics, quotes)
- **CRITICAL:** The blog post content must be 1:1 with the original newsletter (excluding ads). Do not summarize, paraphrase, or restructure. Copy the exact content, structure, and formatting.
- **IMPORTANT:** Use WebFetch with a prompt to extract ALL important links:
- GitHub repository URLs (especially for open source project posts)
- YouTube video URLs (these need to be embedded as iframes)
- Links to other newsletter posts (convert to internal blog links if already converted)
- Any other substantive links mentioned in the content
### 2. Get Publication Date
**Format:** `YYYY-MM-DD`
**Substack:**
- Fetch the archive page: `https://didierrlopes.substack.com/archive`
- Use raw mode and extract post titles alongside dates with pattern matching
- Dates appear as short format (e.g., "Feb 24", "Mar 6") next to post titles
- The slug may contain a date suffix (e.g., `the-era-of-on-demand-software-26-01-31`) but **do NOT use it** - it may not match the actual publication date
- Always verify against the archive page
**Beehiiv:**
- Extract the publication date from https://didierlopes.beehiiv.com/ main page
- Find the newsletter post by title and get its publication date
**Rules:**
- Never use today's date - always use the actual publication date
- Create slug from title: lowercase, replace spaces with hyphens, remove special characters
- Example: "The trampoline job: Optimize your career for growth" → `2025-09-19-the-trampoline-job-optimize-your-career-for-growth.md`
### 3. Create Front Matter
```yaml
---
slug: <title-slug-without-date>
title: <Full Newsletter Title>
date: <YYYY-MM-DD>
image: /blog/YYYY-MM-DD-slug/hero.webp
tags:
- <relevant-tag-1>
- <relevant-tag-2>
- <relevant-tag-3>
description: <Newsletter subtitle or first paragraph summary (max 160 chars)>
hideSidebar: true
---
```
**Front Matter Rules:**
- **slug**: Use title in kebab-case without the date prefix
- **title**: Exact newsletter title, properly capitalized
- **date**: Newsletter publication date in YYYY-MM-DD format (from archive page, NOT today's date)
- **image**: Points to the hero image path inside the post asset folder (WITH `.webp` extension). If the post has no cover image, omit this field
- **tags**: Extract 3-6 relevant tags from content themes (lowercase)
- **description**: Use newsletter subtitle or create compelling summary
- **hideSidebar**: Set to `true` for blog posts
### 4. Process Images
**IMPORTANT: All images must be in WebP format for optimal file size and fast page loads.**
**Image Handling Rules:**
1. **Hero Image**
- **Substack:** Fetch from the archive page (`https://didierrlopes.substack.com/archive`). Extract thumbnail image URLs which follow the pattern with `public%2Fimages%2F<uuid>`. Download at high resolution using: `https://substackcdn.com/image/fetch/w_1200,c_limit,f_png,q_auto:good/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F<uuid>_<dimensions>.<ext>`
- **Beehiiv:** Fetch the hero image from https://didierlopes.beehiiv.com/ main page
- Some posts may use YouTube thumbnails as cover images (URL pattern: `https://substackcdn.com/image/youtube/w_728,c_limit/<video_id>`)
- Download to `/static/blog/YYYY-MM-DD-slug/hero.png` (or `.jpg`) first, then convert to WebP
- Note: The image must be saved in the post's `static/blog/YYYY-MM-DD-slug/` directory, not just `/blog/`
- If a post has no cover image at all, omit the `image:` field from front matter
2. **Content Images**
- Download to `/static/blog/YYYY-MM-DD-slug/N.png` (where N is sequential number)
- Download from Substack/Beehiiv CDN URLs
- Maintain aspect ratio
- Note: Save in the post's `static/blog/YYYY-MM-DD-slug/` directory
3. **Convert All Images to WebP**
After downloading images, convert them to WebP format using:
```bash
# For PNG/JPG images:
cwebp -q 90 "image.png" -o "image.webp" && rm "image.png"
# For GIF images (animated):
gif2webp -q 90 "image.gif" -o "image.webp" && rm "image.gif"
```
Final filenames should be:
- Hero: `/static/blog/YYYY-MM-DD-slug/hero.webp`
- Content: `/static/blog/YYYY-MM-DD-slug/N.webp`
4. **Image Markdown Format**
```markdown

<!-- For centered images with custom width -->
<p align="center">
<img width="500" src="/blog/YYYY-MM-DD-slug/image-path.webp" alt="Description" />
</p>
```
### 5. Convert Content Structure
**Content Conversion Rules:**
1. **Opening Section**
- Do NOT add hero image in the content (it's handled automatically via the front matter `image` field)
- Include newsletter subtitle as opening paragraph
- Add `<!-- truncate -->` after intro paragraph for blog preview
2. **Section Dividers**
- Generally not needed between sections
- Let content flow naturally without visual breaks
- Only use if there's a major topic shift that requires clear separation
3. **Headings**
- Newsletter H3 → Blog H2 (`##`)
- Newsletter H4 → Blog H3 (`###`)
- Maintain heading hierarchy
4. **Lists**
- Preserve bullet points as markdown lists (`-`)
- Maintain indentation for nested lists
- **Important:** Add `<br />` after each list block for proper spacing
Example:
```markdown
- First item
- Second item
- Third item
<br />
Next paragraph starts here...
```
5. **Links**
- **Substack:** Remove tracking parameters (`?utm_source=...&utm_medium=...&utm_campaign=...`)
- **Beehiiv:** Convert Beehiiv tracking URLs to original URLs
- Format: `[link text](url)`
- For tweets/social embeds, reference as: `This [post](url)`
- Convert links to other newsletter posts to internal blog links if already converted (e.g., `https://didierlopes.com/blog/<slug>`)
6. **Emphasis**
- Bold text: `**text**`
- Italic text: `*text*`
7. **Quotes and Citations**
- Use blockquote syntax (`>`) for extended quotes
- For multi-paragraph quotes, use `> <br />` between paragraphs
- Add `<br />` after the quote block
- **IMPORTANT:** Extract the actual hyperlink from the newsletter, not guess or create new ones
- Include attribution with author name and link when available
Example:
```markdown
> First paragraph of the quote goes here.
>
> <br />
>
> Second paragraph of the quote continues here.
<br />
**Author Name** - ["Article Title"](https://actual-link.com)
```
8. **Code/Technical Content**
- Wrap technical terms in backticks: \`term\`
- Use code blocks for snippets
9. **YouTube Videos**
- **IMPORTANT:** Extract the actual YouTube URL from the newsletter content
- Convert YouTube links to embedded iframe format
- Extract video ID from URL (e.g., `https://www.youtube.com/watch?v=VIDEO_ID` → `VIDEO_ID`)
- Use only the video ID in the embed URL, no additional parameters
- Use responsive embed with centered layout
- **IMPORTANT:** Add `<br />` after the video embed so following text doesn't appear glued to it
Example conversion:
- Original: `https://www.youtube.com/watch?v=Zyw-YA0k3xo`
- Embed format:
```html
<div className="flex place-items-center justify-center items-center rounded-sm mx-auto">
<iframe
src="https://www.youtube.com/embed/Zyw-YA0k3xo"
width="800"
height="400"
/>
</div>
<br />
```
**Note:** Always verify the video link exists in the original content - don't assume or guess video IDs
10. **Image Captions**
- If text immediately following an image is a caption/description of that image, style it differently
- Use smaller font size and bring it closer to the image with negative margin
Example:
```html
<p align="center">
<img width="800" src="/blog/YYYY-MM-DD-slug/image.webp" alt="Description" />
</p>
<p align="center" style={{fontSize: '0.85em', marginTop: '-0.5em'}}>Caption text describing the image above.</p>
```
### 6. Content Cleanup
**Remove from Newsletter:**
- Footer/unsubscribe links
- Newsletter-specific CTAs (subscribe buttons, share widgets)
- Tracking parameters from URLs (`?utm_source=...`)
- Newsletter metadata (view in browser links)
- Substack "No posts" artifacts at the bottom
- "A quick note: I've moved this newsletter from Beehiiv to Substack" migration notices (unless contextually important)
**Preserve:**
- Author voice and tone
- All substantive content
- External references and citations
- Story flow and narrative structure
### 7. Quality Checks
Before finalizing:
- [ ] Front matter is complete and valid YAML
- [ ] Front matter `image:` field includes `.webp` extension (or is omitted if no cover image)
- [ ] All images are downloaded, converted to WebP, and properly referenced
- [ ] All image references in content use `.webp` extension
- [ ] Links are clean (no tracking parameters)
- [ ] Markdown syntax is valid
- [ ] Content flows naturally without newsletter artifacts
- [ ] File is saved in `/blog/` directory with correct naming
- [ ] Test render locally to ensure formatting
## Example Conversions
**Substack URL:** `https://didierrlopes.substack.com/p/the-context-wars-in-financial-services`
**Converted to:** `/blog/2026-02-13-the-context-wars-in-financial-services.md`
**Beehiiv URL:** `https://didierlopes.beehiiv.com/p/the-trampoline-job-optimize-your-career-for-growth`
**Converted to:** `/blog/2025-09-19-the-trampoline-job-optimize-your-career-for-growth.md`
**Key transformations:**
- Extracted images and converted to WebP
- Converted newsletter sections to H2/H3 headings
- Cleaned URLs of tracking parameters
- Added proper front matter with relevant tags
- Preserved personal narrative and bullet points
## Common Pitfalls to Avoid
- Don't include newsletter-specific language ("Click here to read more")
- Don't forget to download images (CDN links may expire)
- Don't use relative dates ("last week") - use specific dates
- Don't include email-specific formatting (table layouts for email clients)
- Don't forget the `<!-- truncate -->` marker for blog preview
- Don't trust date suffixes in Substack slugs - always verify against the archive page
- Don't use `substack.com/home/post/p-<id>` URLs directly for content extraction - try `didierrlopes.substack.com/p/<slug>` first as it renders better
Ver en GitHub