Backup Everything at Once with RSS
Warning
My Ghost blog is not serverless. Running a blog on a server takes constant upkeep, but it has a lot going for it, and one big drawback. If the server crashes, getting the posts back out is really hard. As the blog grows, copying every post and photo by hand would get way too tedious. So I wanted a better way.
Problems with Ghost's Built-in Backup
Ghost lets you download a backup file in .json format. It's a full copy of the blog's soul. Everything you can set in Ghost goes in as is, the author's name, the tags, post content and format, upload time, even the summary in the HTML meta tags.
But there are two problems.
- A human can barely read the file. The JSON is minified, the structure is complicated because it holds so much, and the posts are compressed.
- It doesn't back up photos. Restore the blog and every photo shows "not found" (broken images, as everyone calls them). If the server is still alive, or you copied the photos somewhere, you're in luck. If not, some photos may be gone for good.
Goals
Main Goal
- Both posts and photos should be backed up. ★★★
Bonus Goals
- It should be in a human-readable format. (Human-Readable Medium) ★★☆
- It should be clear which photo goes into which location of which post, in preparation for restoring the blog.
- Backup should be convenient. ★★☆
- It should be possible to create a replica outside the blog. ★★☆
Idea
The answer is RSS. RSS came out of the blogging boom of the early 2000s as a way to "subscribe" to sites. Sites or blogs that support RSS provide an RSS feed address. The RSS feed address contains updated content from that site in a machine-readable format. When users enter the RSS feed address into an RSS reader, the reader fetches new content from the RSS feed address each time.
Social media has mostly made RSS obsolete, but it does everything I need here. The feed works as an API for getting posts. Ghost supports RSS by default, so I decided to use it.
General Idea
- Enter the blog's RSS address and copy the whole feed.
- Parse it and extract each post's HTML.
- Create a folder per post and save its HTML there.
- Download photos from the
srcaddresses in the post'simgtags. - For posts with photos, create an
imagesfolder inside the post folder, save the photos there, and point eachimgtag'ssrcat the relative path.
Development
Reference
All the examples below are based on v1 of anaclumos/backup-with-rss. By the time you read this, new features or bug fixes may have been added.
Also, the code included in this post is intended to show a general flow, not the entire code. If you try to copy and run it as is, it probably won't work! The complete code is available in the GitHub repository.
1. Copying the RSS Feed Using Feedparser
I copied the RSS feed with Python's Feedparser module.
RSSReader loads the feed and hands back its entries.
Here's all the code does.
- Creating an RSSReader stores the RSS address in
self.origin, parses it, and stores the result inself.feed. parse()returns theentriesfromself.feed.
The entries hold the feed's posts as a list. The example below is the RSS for this post.
Structure of self.feed.entries in parse()
2. Creating Markdown Files from RSS Data
I only needed a few values from the self.feed.entries that RSSReader returns, so I made an MDCreator class to work through them.
The blogDomain parameter is used later.
Here's all the code does.
- Creating an MDCreator stores the blog address in
self.blogDomainand the raw feed data inself.rawData. That raw data is theself.feed.entriesfrom RSSReader'sparse(). createFile()makes a folder per post inside the backup folder, named after the post's title. Each folder gets aREADME.mdwith the post inside.
Files are created through the codecs library so Windows uses Unicode instead of the CP949 codec. That's what makes emojis in the RSS show up correctly 🚀🥊
3. Adding Post Information to the Generated Markdown File
For the post information I went with Jekyll-style Front Matter. It seemed like the easiest way to check a post's title, tags, link, author, and the rest.
Here's all the code does.
- It checks if the post's title, tags, link, ID, author names, and publication date are in the RSS, and puts what it finds into the Front Matter.
- If a value is missing it writes
~ Unknowninstead.
Tags take the odd code like self.getValueListOfDictList(self.rawData.tags, "term") because Ghost writes them in this format. Gatsby and WordPress do the same.
This pulls only the term out of tags and adds it to the Front Matter. Run it and you get Front Matter like this.

And this is how GitHub renders it.
4. Adding Post Summary and Content to the Generated Markdown File
Next the Summary and Content from the RSS go into renderedData.
One interesting thing. Ghost and WordPress support both RSS Summary and Content, but Jekyll-based GitHub Pages and Tistory stuff the whole post into the Summary. (...) Ghost lets you set a post's Excerpt, and that becomes the Summary.
5. Adding Images to the Generated Markdown File
A backup is only whole if the images survive too. Unless an image is embedded in the HTML as base64, it's just an img tag with a src. Once the server goes down that src loads nothing, so every image has to be downloaded during the backup.
I leaned on How to Download All Images from a Web Page in Python by PythonCode for this part.
Here's all the code does.
- It reads the
renderedDatastring as HTML and finds everyimgtag. - It checks for a
srcordata-srcattribute.data-srcis there for WordPress compatibility. - It makes an images folder inside each post folder and saves the images there. The file name comes from the last part of the src, so
https://blog.someone.com/images/example.pngbecomesimages/example.png. - It rewrites the src to that relative path.
- If there's a
srcset, it removes it, for Gatsby compatibility.
One problem is image addresses are all over the place. Some sites write the full domain, like <img src = "https://example.png/images/example.png">. Others start from a subdirectory, like <img src = "/images/example.png">. A few just write <img src = "example.png">. To cover as many of these as I could, I wrote an isDomain() function that spots the domain. Other libraries kept reading file extensions like .png as top-level domains like .com, so I added some exception handling.
When an address can't be reached directly, like <img src = "/images/example.png">, the domain gets added in front. This is where self.blogDomain comes in.
Results
I tried it on this blog, a self-hosted Ghost blog. Just run main.py and the backup rolls on.



Testing says these services work. A post's format or layout may shift a little, but the backup itself does its job.
- Ghost
- WordPress
- Jekyll-based GitHub Pages
- Gatsby-based GitHub Pages
- Medium
- Tistory
Goal Achievement Evaluation
Main Goal
-
Both posts and photos should be backed up. (3/3)
Fully done. Videos don't get backed up, but videos usually come in through YouTube embeds anyway, so the chance of losing anything that way is much lower. That's why they were out of the goal from the start.
Bonus Goals
-
It should be in a human-readable format. (Human-Readable Medium) (2/3)
Compared to Ghost's own backup, the important bits sit right in the Front Matter and posts render almost like they do on the blog. Folders make posts and photos easy to find too. Editing is the awkward part. The file is Markdown but the body is HTML. It does just what Lots of copies keep stuff safe asks for.
-
It should be clear which photo goes into which location of which post. (In preparation for restoring the blog) ★★★
It is clear which photo goes into which location of which post.
-
Backup should be convenient. (2/3)
main.pyneeds to be executed manually. I'm thinking of automating it withcrontabsomeday.
One catch comes from RSS itself. Only posts in the feed get backed up. Feeds usually carry just the latest posts to save bandwidth, and each blog has an option to adjust that. A Ghost blog puts its latest 15 posts in the feed by default, and you can't change that count inside the Ghost CMS. You have to touch the code of Ghost Core.
-
It should be possible to create a replica outside the blog. (2/3)
Download a lot of photos from a WordPress blog over and over, and access can get blocked for a while.
Future Plans
Sitting with the finished thing for a bit, I started thinking it could help people who want to move their blog but are stuck with a lot of piled-up data. The plan from here is to keep improving it in that direction, into a tool that helps with blog migration.
References
- How to Download All Images from a Web Page in Python by PythonCode
- Ghost Custom RSS Feed
- If the feed contains unsupported Unicode characters, the script won't run properly. An editor or blog error usually causes this, and a service like Feed Validator can find those characters so you can fix them and get things running again.