Today I finished the first stable version of something I have wanted for a long time: a local backup of my social-media posts.
The goal was deliberately simple. I wanted the things I had published on Instagram to remain mine even if I eventually stopped using Meta. Not an Instagram embed, not a live connection, not an API that could disappear next year. A real copy, stored locally on elkin.de, with the text, dates, photos and videos intact.
The result now lives at /dev/socialmedia.
Importing Instagram without connecting to Instagram
The system is a separate WordPress plugin called MB Social Media Backup. It never connects to Instagram, Meta or any other social network. There are no API keys, OAuth tokens, background synchronisation jobs or remote embeds.
Instead, I request the official export from Instagram, upload the untouched ZIP to the server through SFTP and let WordPress process it locally.
The first archive was approximately 315 MB, which immediately made a normal browser upload impractical. The plugin therefore uses an inbox folder on the server and extracts the archive in small batches. It can survive timeouts, resume interrupted imports and process several hundred posts without trying to load the entire archive into memory at once.
The temporary working directory can be removed when no import is active. The original ZIP remains separate, while the useful copy is stored inside WordPress.
Reconstructing the posts
Importing the files was only the beginning.
Meta’s export format contains enough information to rebuild the archive, but not always in an especially convenient shape. Text encoding had to be repaired. Media paths had to be matched to the correct posts. Duplicate images had to be detected without accidentally deleting legitimate carousel items.
Stories, Reels and normal Feed posts also needed to be distinguished. This proved more difficult than expected because the relevant information was sometimes hidden in the media path rather than in an obvious content-type field.
Stories were another interesting problem. Instagram exports each Story item separately, even when several were posted together on the same day. The importer now groups those daily Story sequences into a single local post with multiple media items.
Hashtags in the original captions are converted into native WordPress tags. The original caption remains unchanged, but the archive can now use WordPress taxonomy for future filtering and navigation.
A social feed without a social network
The public archive is not displayed like a normal blog category. It has its own feed designed to feel closer to a social-media app.
Posts appear in a central column and load progressively while scrolling. Images, videos and carousels are served directly from elkin.de. Nothing is requested from Meta.
Videos use the browser’s native player. Vertical videos can use the available width, while horizontal videos are constrained so that one post does not consume several screens. Stories preserve their full height because text and stickers are often part of the image itself and must not be cropped.
The infinite scroll caused a surprisingly stubborn series of problems. At first the next batch never loaded. Then the fallback button did not appear. The JavaScript was blamed several times, but the real problem was a WordPress query filter reporting that only eight posts existed.
The final solution was wonderfully unfashionable: for this specific feed, the plugin reads the published post IDs directly and asks for one extra result to determine whether another batch exists. Simple, predictable and no longer vulnerable to unrelated filters elsewhere on the site.
The technologies
The archive is built on WordPress, because the site already has years of content, a media library, taxonomies and a publishing interface worth keeping.
The importer is written in PHP and uses WordPress custom post types, metadata, attachments and taxonomies. Each social post becomes its own mb_social_post, while the media files become normal local WordPress attachments.
The large imports use ZIP processing, resumable batches and temporary manifests. This avoids browser upload limits and reduces memory usage.
The frontend uses semantic HTML, custom CSS and a small amount of vanilla JavaScript. No JavaScript framework was necessary. The browser’s IntersectionObserver helps trigger loading near the bottom, with scroll detection and a manual button as backups.
Media is displayed with native HTML image and video elements. This keeps the archive independent, accessible and far easier to maintain than reproducing Instagram’s interface with a large frontend framework.
The database is MariaDB, through WordPress’s normal MySQL-compatible layer. The actual photos and videos live in the WordPress uploads directory rather than inside the database.
SFTP is used for transferring large official exports to the server. This avoids PHP upload limits and keeps the archive workflow explicit: I download my data, I upload my data, and my server processes my data.
The problems we found
Several things broke along the way.
The first importer created the posts but skipped their media during later repair runs. Some German characters arrived as encoding debris. A photograph appeared twice because Meta referenced the same file through more than one JSON path.
The first carousel logic was also too optimistic. It could display a carousel, but very few posts were actually being grouped correctly. A diagnostic report eventually showed exactly how many posts contained one, two or several media files, which made the remaining mistakes much easier to identify.
The mobile menu also returned to haunt us more than once. On special archive pages, WordPress occasionally rendered the hidden navigation content before its proper block styles were available. The result was a collection of enormous black icons occupying most of the screen. The current plugin includes a defensive fix, although this is one of the areas I will continue watching.
Version numbers caused another avoidable problem. A ZIP labelled as a newer release still identified itself internally as the previous version, which meant WordPress and the browser could keep old code in place. The plugin now updates its header, internal version and asset versions together.
Most of the work was not difficult in isolation. The difficult part was preserving hundreds of old posts while repeatedly changing the importer without creating duplicates or losing media.
