The Easy Way to Bring Medium Posts to Astro
Writing on Medium is great. With its discovery, audience, and comment system, it remains one of the most appealing writing platforms. But what if these articles could also be listed on a personal site? Manually copying and pasting, updating pages for each new post, trying to keep content in sync… it is an unsustainable effort.
That is exactly where @ykocaman/astro-medium-loader comes in. This post covers what the extension does, what need it addresses, how to install and use it, and how it works on this very site.
Source: astro.build
Astro Content Layer + Medium RSS
With astro@5.0, Astro introduced the Content Layer API. This API allows content collections to be fed not only from local files but also from remote sources. @ykocaman/astro-medium-loader uses this API to turn a Medium RSS feed into an Astro content collection.
In other words, it reads the RSS feed from a Medium profile, parses it, and makes it queryable via Astro’s getCollection('medium') API. The result? Medium articles are listed, paginated, and rendered on the site just like local MDX files.
How It Works
At the heart of the extension are two core components:
| Component | Role |
|---|---|
mediumLoader() | Defines a loader for the Astro Content Layer; accepts username and storage parameters |
rss-parser | Reads and parses the RSS feed from https://medium.com/feed/@username |
The flow is as follows:
flowchart LR
Medium[Medium RSS Feed] -->|rss-parser| Loader[mediumLoader]
Loader -->|Zod schema| Collection[Astro Content Collection]
Collection -->|getCollection| Site[Static Site Pages]
Collection -->|render| Post[Blog Post View]For each Medium article, the following data is retrieved: title, original link, publication date, description, full HTML content, categories, and a hero image if available. A canonical link is automatically added to the end of each article. This is a critical detail for SEO.
Installation
Run the following command in the project directory:
pnpm add @ykocaman/astro-medium-loader
# or
npm install @ykocaman/astro-medium-loaderAn Astro version of ^5.0.0 is sufficient. The extension requires this as a peer dependency.
Usage
Usage consists of adding a collection definition to the content.config.ts file. It can work with three different storage strategies:
1. Live Fetch (Every Build)
The Medium RSS feed is fetched again on every build or request. Suitable for small, frequently updated profiles:
// content.config.ts
import { defineCollection } from 'astro:content';
import { mediumLoader } from '@ykocaman/astro-medium-loader';
const medium = defineCollection({
loader: mediumLoader({ username: 'ykocaman' })
});
export const collections = { medium };2. Cached Usage (Recommended for Development)
The feed is cached under .astro/storage/medium. During development, no repeated requests are made, but since the .astro directory is git-ignored, fresh data is fetched with every deploy:
const medium = defineCollection({
loader: mediumLoader({
username: 'ykocaman',
storage: { enabled: true, path: '.astro/storage/medium' }
})
});3. Persistent Storage (CI and Offline Builds)
Data is written to a permanent directory such as src/content/medium/. No requests are made to Medium after the first build. Ideal for CI environments or offline builds:
const medium = defineCollection({
loader: mediumLoader({
username: 'ykocaman',
storage: { enabled: true, path: 'src/content/medium' }
})
});Rendering the Collection
Once defined, it can be used with getCollection and render on any Astro page:
---
import { type CollectionEntry, getCollection, render } from 'astro:content';
import BlogPost from '../../layouts/BlogPost.astro';
export async function getStaticPaths() {
const posts = await getCollection('medium');
return posts.map((post) => ({
params: { slug: post.id },
props: post,
}));
}
type Props = CollectionEntry<'medium'>;
const post = Astro.props;
const { Content } = await render(post);
---
<BlogPost {...post.data}>
<Content />
</BlogPost>How It Is Used on This Site
On ykocaman.dev, @ykocaman/astro-medium-loader combines local blog posts with Medium articles under a single roof. Here is how it works:
src/content.config.ts defines two separate collections:
const blog = defineCollection({
loader: glob({ base: './src/content/blog', pattern: '**/*.{md,mdx}' }),
schema: ({ image }) => z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: image().optional(),
}),
});
const medium = defineCollection({
loader: mediumLoader({
username: MY_USERNAME,
storage: { enabled: true }
}),
});
export const collections = { blog, medium };src/pages/blog/[...slug].astro is a dynamic route that scans both collections. Medium articles take priority; local posts with the same slug get a local- prefix to avoid conflicts.
src/utils/blog.ts contains the getAllPosts() function that merges both collections and sorts them by date. A source: 'local' | 'medium' field distinguishes the origin of each post.
This way, the blog list on the homepage displays both local and Medium articles using the same card design.
Highlighted Features
| Feature | Description |
|---|---|
| Automatic RSS parsing | Medium feed is reliably parsed with rss-parser |
| Canonical link | Original Medium link is appended to each article, eliminating SEO penalty risk |
| Flexible caching | Live, cached, or persistent storage options |
| Full Astro integration | getCollection, render, Zod schema validation — all through native Astro APIs |
| Type safety | Written in TypeScript; CollectionEntry<'medium'> type is fully supported |
| MIT license | Open source, free for commercial use |
What Needs Does It Address?
This extension is particularly useful in the following scenarios:
- Personal blog + Medium author: Articles are published on Medium but also need to be archived and listed on a personal site.
- Portfolio sites: Technical articles are written on Medium and displayed as a “Writing” section on a portfolio site.
- Content aggregation: Compiling Medium content from multiple authors on a single site.
- SEO strategy: Sending the right signals to search engines by serving canonical-linked content on a custom domain.
- CI/CD friendly: Eliminating external request dependencies during builds with persistent storage mode.
Technical Details
The extension uses the rss-parser library to parse XML from https://medium.com/feed/@username. The following data is extracted from each RSS item:
- slug: A URL-friendly version of the title
- heroImage: From the first
<img>tag in the content - description: A summary of the first 32 words
- content: HTML from Medium’s
content:encodedfield (the “was originally published in” block is cleaned since a canonical link is added) - canonical: HTML pointing to the original Medium link
All data is validated through the Zod schema. Type-safe default values are assigned for any missing fields.
Wrapping Up
@ykocaman/astro-medium-loader is a lightweight, open-source solution designed to effortlessly list Medium content on an Astro site while continuing to write on Medium. It can be installed and configured in 5 minutes, and its storage strategies make it suitable for both development and production environments.
If having Medium articles live on a personal site sounds useful, this extension is a great fit.
Links
You can read this post in Turkish.