I spent three hours last Tuesday staring at my RSS feed, watching it dutifully render everything except the images I’d carefully imported into my MDX files. The book covers I’d referenced, the diagrams I’d created—all of them replaced with broken references like {bookCover.src}. Beautiful.
Here’s the thing: I’m not a programmer. I’m a UX designer who’s comfortable enough with code to be dangerous, but diving into Astro’s rendering pipeline and TypeScript configurations? That’s not my daily work. But RSS feeds are having a moment again, and I wanted my site’s feed to respect that. Full content. Proper formatting. Working images.
So I did what made sense: I turned to Claude Code as my co-pilot. If AI is genuinely good at anything, it’s navigating code that would otherwise intimidate someone like me. This isn’t a story about me heroically solving a complex technical problem alone—it’s about using the right tools to bridge the gap between what I understand (UX, content structure, web design) and what I needed to figure out (Astro’s Container API, TypeScript configurations, regex patterns for image paths).
Turns out that even with AI assistance, that last part is harder than it should be.
The disconnect
Astro’s official RSS integration handles basic markdown beautifully. You point it at your content collections, and it generates a perfectly functional feed. But here’s the thing: sometimes I write in MDX. Not because I enjoy complexity, but because I import images, use custom components, and occasionally need JSX when plain markdown won’t cut it.
Standard markdown parsers look at MDX and see nonsense. When they encounter something like <img src={bookCover.src} alt="Book cover" />, they either render it as literal text or strip it entirely. The imported image? Gone. The carefully optimized asset that Astro processed during build? Nowhere to be found.
I tried the obvious fixes first. Strip the imports, hope the references resolve somehow. Parse it as HTML. Use a different markdown library. Nothing worked quite right. The images either broke in development, failed in production, or simply never appeared.
What I actually needed
The problem runs deeper than just rendering. MDX files aren’t just markdown with imports — they’re JavaScript modules that need proper compilation. When you write import bookCover from '../../assets/image.webp', Astro transforms that into an optimized asset with a hashed filename. That transformation happens during build, which means any RSS generation that happens outside that pipeline sees unresolved references.
I needed something that could:
- Actually render MDX, not just parse markdown
- Handle imports properly
- Resolve image paths correctly in both development and production
- Maintain Astro’s image optimization
The official documentation wasn’t much help and most RSS tutorials assume markdown. The few that mention MDX either skip images entirely or suggest workarounds that feel fragile.
The Container API
Astro v4.9 introduced something called the Container API. It’s marked as experimental, which initially made me hesitate — I’ve been burned by experimental features before. But the core idea made sense: it allows rendering Astro components outside of .astro files.
That’s exactly what RSS generation is: rendering content outside the normal page context.
This is where having Claude Code as a thinking partner became invaluable. I could explain what I was trying to accomplish—“I need my MDX files to render properly in RSS feeds with all the images working”—and it could navigate the Astro documentation, suggest implementation approaches, and help me understand what each piece of code was actually doing. Not just “copy this code,” but “here’s why this approach works and here are the trade-offs.”
The key insight (which took some back-and-forth to understand) is that MDX files in Astro are essentially components. When you render them through the Container API, they go through the same processing pipeline as they would in a regular page. Imports get resolved. Images get optimized. JSX gets compiled.
The TypeScript hurdle
First obstacle: the Container API needs moduleResolution: "bundler" in your tsconfig.json. I had mine set to "node" like most tutorials recommend. One line change:
{
"compilerOptions": {
"moduleResolution": "bundler"
}
}
Setting up the container
The expensive part of using the Container API is creating the container itself. You don’t want to do this for every single post. Create it once, reuse it everywhere:
import { experimental_AstroContainer as AstroContainer } from 'astro/container';
import { getContainerRenderer as getMDXRenderer } from '@astrojs/mdx';
import { loadRenderers } from 'astro:container';
let mdxContainer = null;
async function getMdxContainer() {
if (!mdxContainer) {
const renderers = await loadRenderers([getMDXRenderer()]);
mdxContainer = await AstroContainer.create({ renderers });
}
return mdxContainer;
}
Smart content processing
The actual rendering logic needs to distinguish between regular markdown (which can use a fast parser) and MDX (which needs the Container API):
async function processContent(entry, siteUrl) {
const isMdxFile = entry.filePath?.endsWith('.mdx') ?? false;
if (isMdxFile) {
const { Content } = await render(entry);
const container = await getMdxContainer();
let htmlContent = await container.renderToString(Content);
// Handle development image paths
if (htmlContent.includes('/@fs/')) {
htmlContent = htmlContent.replace(
/\/@fs\/[^"'\s]+/g,
(match) => {
const urlMatch = match.match(/\/([^/?]+\.(webp|jpg|jpeg|png|gif))(\?[^"'\s]*)?/);
if (urlMatch) {
const params = urlMatch[3] || '';
return `${siteUrl.origin}/_image?href=${encodeURIComponent(match)}${params ? '&' + params.slice(1) : ''}`;
}
return match;
}
);
}
return htmlContent;
}
// Regular markdown - use the fast path
let content = entry.body?.replace(/^import.*$/gm, '') || '';
return parser.render(content);
}
That regex for image paths? That’s handling the difference between development (where Astro serves images through /@fs/) and production (where everything gets hashed into /_astro/). In development, we convert those virtual filesystem paths to proper image endpoints. In production, Astro’s build process handles it automatically.
This was one of those moments where Claude Code really proved its worth. I understood the concept — paths need to work differently in dev vs production — but writing a regex pattern that correctly matches and transforms those paths? Not something I do regularly. Having an AI that could generate the pattern, explain what each part does, and then help me test it with different edge cases made this tractable instead of frustrating.
The parts that still feel rough
The Container API works, but it’s not perfect. Adding it to my build increased generation time by maybe 8-10 seconds for about 50 posts. Not terrible, but noticeable. And because the API is experimental, there’s always that nagging concern about future breaking changes.
I also had to be more careful about error handling. If MDX rendering fails for any reason — a bad import, a syntax error — the entire RSS generation could break. My solution was to catch errors and fall back to basic markdown parsing:
try {
// Try MDX rendering
const { Content } = await render(entry);
const container = await getMdxContainer();
htmlContent = await container.renderToString(Content);
} catch (mdxError) {
console.warn('MDX rendering failed for', entry.id);
// Fall back to markdown parser
let content = entry.body?.replace(/^import.*$/gm, '') || '';
htmlContent = parser.render(content);
}
Not elegant, but pragmatic. Better to have a feed with basic markdown than no feed at all.
What actually changed
My RSS feed now includes the full content of every post, images and all. Readers using apps like NetNewsWire or Reeder see the complete article without needing to click through to the website. The images aren’t broken references—they’re actual, optimized images that load properly.
More importantly, I’m not fighting the tool anymore. The solution fits within Astro’s architecture instead of working around it. Regular markdown posts still use the fast parser. MDX posts get the full rendering pipeline. The system detects the file type and handles it appropriately.
If you’re dealing with this
The complete code is longer than I want to paste here, but the pieces I’ve shown are the essential parts. The rest is mostly constructing XML, handling dates, that sort of thing.
A few things I learned:
- The Container API is stable enough for production despite the “experimental” label. It’s been working reliably since I got it running.
- Build time impact is real but manageable. Consider caching the RSS feed at the CDN level if you’re worried about performance.
- Test your feed in actual RSS readers, not just by looking at the XML. Some readers handle images differently.
- Keep your MDX simple in content files. The more complex your components, the more likely you are to hit edge cases.
- If you’re not a programmer, don’t let that stop you. Tools like Claude Code can bridge that gap, helping you understand the code you’re implementing rather than just blindly copying it.
The Astro docs have more details on the Container API if you want to dig deeper. The implementation isn’t as complicated as it first appears, but it does require understanding how Astro processes MDX files.
I’m curious if anyone else has tackled this problem differently. RSS seems like it should be straightforward, but the reality is messier when you start combining modern tooling with a format designed decades ago. That tension between old standards and new workflows creates these odd gaps where the obvious solution doesn’t quite exist yet.
What’s interesting to me, as someone who isn’t a programmer by trade, is how AI tools like Claude Code are changing what’s possible for designers and other non-developers to accomplish. Five years ago, I would have lost an absurd number of hours trying to figure this out or simply given up. Today, I could work through it with an AI assistant that understood both my goal and the technical implementation needed to get there. That feels like a meaningful shift in what’s achievable when you’re willing to learn but don’t have a CS degree.
For now, though, my feeds work. Images show up. MDX renders properly. That’s good enough.