From Substack to Squarespace: A Developer's Guide to a Seamless Migration
How I Actually Pulled This Off
There is no button that says “Move my Substack to Squarespace.”
If you’ve ever tried, you already know that. You get a CSV. You get a pile of HTML files. And Squarespace politely tells you it only wants WordPress XML. That mismatch is where most migrations die.
I just finished moving 734 posts from Substack into Squarespace. This is how I did it, working alongside Manus AI, without losing dates, titles, or content, and without re-publishing everything by hand.
The real problem
Substack gives you two things when you export:
A
posts.csvfile with metadata like post IDs, titles, subtitles, dates, and publish statusA folder full of HTML files, one per post
Squarespace, meanwhile, only really understands WordPress. Its importer expects a WordPress eXtended RSS file (WXR), which is a very specific XML format that WordPress uses to move sites around.
So you have data in two formats that do not speak to each other at all.
CSV and HTML in one corner
WXR XML in the other
Nothing connects them.
How Manus AI and I built the bridge
Instead of trying to hack Squarespace or manually rebuild posts, I sat down with Manus AI and treated this like an integration problem.
Substack already gave me everything I needed. Squarespace already knew how to import WordPress. The only missing piece was a translator.
So Manus AI and I created a small Python script that takes a Substack export and turns it into something Squarespace thinks is a WordPress site.
All it does is:
Read
posts.csvinto a lookup table keyed bypost_idWalk through the HTML files and grab the post ID from each filename
Match each HTML file to its metadata
Wrap that data in the XML structure WordPress expects
Output one giant WXR file that Squarespace can import
No APIs. No plugins. No scraping. Just file transforms.
That is the entire trick.
What the WXR file looks like
WordPress export files are just XML with a specific schema. You need a header that defines the namespaces, a channel block for the site, and then one <item> per post.
At the top it looks something like this:
<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0"
xmlns:excerpt="http://wordpress.org/export/1.2/excerpt/"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:wp="http://wordpress.org/export/1.2/"
>
<channel>
<title>Your Blog</title>
<link>https://yourdomain.com</link>
<description>Your site description</description>
<wp:wxr_version>1.2</wp:wxr_version>
</channel>
</rss>
Squarespace does not care what platform actually generated this. It just checks that the XML matches what WordPress would have exported.
So we made it look like WordPress did.
How each Substack post becomes a WordPress post
Every Substack post becomes one <item> block in the XML. We map:
Substack title to
<title>Substack date to
<wp:post_date>HTML file contents to
<content:encoded>Substack publish status to
<wp:status>
The content goes inside a CDATA block so the HTML does not get parsed as XML:
item_xml = f"""
<item>
<title>{escape_xml(title)}</title>
<link>{post_link}</link>
<pubDate>{pub_date_rfc}</pubDate>
<dc:creator><![CDATA[Author Name]]></dc:creator>
<content:encoded><![CDATA[{html_content}]]></content:encoded>
<wp:post_id>{post_id}</wp:post_id>
<wp:status><![CDATA[{status}]]></wp:status>
<wp:post_type><![CDATA[post]]></wp:post_type>
</item>
"""
Manus AI also helped clean the Substack HTML, stripping out scripts and styles that do not belong in a Squarespace post.
Nothing fancy. Just clean inputs and correct structure.
The moment of truth
When the script finished running, I had one file:
squarespace_import_wxr.xml
It contained all 734 posts with their original titles, dates, and content intact.
In Squarespace:
Settings → Advanced → Import/Export
Choose WordPress
Upload the file
That was it.
Squarespace ingested it like it came from a WordPress blog. Every post landed where it should, backdated correctly, and ready to be styled.
No copy-paste hell. No SEO nuking. No losing a decade of writing.
Why this matters
Platforms are sticky by design. They make it easy to publish and hard to leave. Substack is great at what it does, but if you want to own your site, your SEO, and your long-term archive, you need a way out.
This was not a Squarespace problem or a Substack problem. It was a format problem.
Once you understand that WordPress XML is the lingua franca of content migration, everything else becomes an integration exercise. Manus AI just made that exercise dramatically faster.
And that is really the story here. Not the script. Not the XML. Just the fact that with the right AI at your side, you can move your work wherever you want it to live.