I had 11 old posts sitting on Medium. Since I already had a blog built with Astro, I decided to move them here so the Markdown and images could live with the rest of my site.
I expected this to be mostly copy and paste. The text was easy. Images were not.
I used ChatGPT Work with Codex to do the migration. It was a nice workflow for comparing the old and new posts, fixing things that looked wrong, and then checking the rest of the archive for the same problem.
Frontmatter
Each post became a Markdown file with frontmatter for the title, date, tags, hero image, and original Medium URL:
---
title: 'Building Pixel-Perfect Minesweeper in React'
pubDate: 2020-04-19
tags: ['gatsbyjs', 'netlify', 'react', 'programming', 'css']
img: '@/assets/blog/building-pixel-perfect-minesweeper-in-react/hero.gif'
originalSource:
name: Medium
url: 'https://leyanlo.medium.com/building-pixel-perfect-minesweeper-in-react-51391c5a3061'
---
The original date is important so a post from 2016 does not suddenly look new. The originalSource field adds a small Originally published on Medium note to the post. It is mainly for readers and should not need to be updated unless the Medium URL changes.

Markdown and Tailwind Typography
Most of the formatting came for free from Tailwind Typography. The article body has a prose class, which handles headings, paragraphs, links, lists, blockquotes, and code blocks.

This kept almost all of the imported content as normal Markdown. I only added custom behavior where Markdown did not have enough information.
Figures
Medium can group two or three images into one figure with one caption. Markdown does not have a standard way to express that, so my first import stacked every image vertically and left the caption floating too far below it.
I used the optional image title as a small hint:


_One caption for both screenshots_
A rehype plugin turns consecutive image-group images into one <figure> and converts the following emphasis-only paragraph into a normally styled <figcaption>. The same code centers single images and keeps all captions close to their figures.



The text column is six columns wide on desktop, but grouped figures can extend to ten. This looks much better for comparisons. It did create one weird problem: the wide figure could scroll behind the table of contents. The table of contents now fades out while a wide figure passes through its space.
RSS images
Astro optimizes imported images and gives them hashed URLs under /_astro/. My first RSS feed linked to the original source paths, which did not exist in the built site.
I fixed this by using Vite’s import.meta.glob to get the emitted image URLs and then checked every image in the generated feed. This was probably the least visible bug in the migration, but also the one most likely to remain broken if I only looked at the website.
Working with Codex
The useful part was not having Codex blindly import everything. It was the iteration afterward.
I noticed that two Gmail screenshots should be side by side. Then another post showed that the same fix needed to support three images. Captions were too far away, single images were not centered, wide figures overlapped the table of contents, and a few posts were missing table-of-contents entries altogether.
Each time, I could point out one example, have Codex generalize the fix, and then audit all 11 posts for similar cases. Afterward it ran linting, type checks, tests, a production build, and a noindex staging deploy for visual review.
Canonical links
Once the Astro posts were live, I updated each Medium story under Story settings → Advanced Settings. I enabled This story was originally published elsewhere and entered the matching URL on my site.

The Astro post is now the canonical version for search engines, while the Originally published on Medium note still preserves where the post came from. I waited until the new URLs returned 200 before changing Medium so none of the canonical links pointed at missing pages.
Overall, the migration was pretty painless. The posts are now plain Markdown, the images are local and optimized, and the few special layout rules work for future posts instead of being hardcoded into these 11.
