WP Manifestindependent plugin directory
manifest / content / simple-export-import

Simple Export & Import

A simple plugin to export and import WordPress posts with full data preservation.

by Vitalii Kaplia · github.com/vitaliikaplia/simple-export-import · website

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/vitaliikaplia/simple-export-import/archive/refs/heads/master.zip

Readme

Simple Export & Import

A WordPress plugin for exporting and importing posts as JSON with full Gutenberg / ACF / multilingual support, optional embedded media with ID remapping, and GitHub-backed auto-update.

Description

Simple Export & Import exports any post to a portable JSON file and imports it back on another site without losing data — even when the donor and recipient sites are decoupled. It correctly preserves:

  • Gutenberg blocks (including InnerBlocks and nested structures)
  • ACF blocks (the JSON inside `` block comments)
  • ACF Repeater / Group / Flexible Content fields (via _field_* references)
  • Custom fields (post meta), taxonomies, featured image, attached media
  • Cyrillic and other UTF-8 content (no escaping)
  • WPML translations and WP-LOC translations (any plugin exposing the WPML hook surface) — bidirectional
  • Per-language attachment translations (one file, multiple language rows) — WPML Media Translation model
  • Yoast SEO postmeta (and the wp_yoast_indexable cache that backs it)

Why a dedicated plugin?

Two pitfalls of naive post-export-import scripts:

  1. Theme/plugin save_post hooks mutate imported content, throw validation errors, hit external APIs, or recurse. This plugin imports via direct $wpdb writes, deliberately bypassing save_post, transition_post_status, added_post_meta, set_object_terms, and friends — content lands exactly as exported.
  2. wp_unslash() strips backslashes from Gutenberg/ACF block JSON when content goes through wp_insert_post() / update_post_meta(). The plugin sidesteps the whole wp_unslash problem by writing directly via $wpdb (which does not unslash) — so ACF block JSON attributes and any string with \ survive intact.

After the raw write, the plugin selectively re-invokes the few SEO-specific rebuilders (Yoast Indexable_Builder, Rank Math link cache) that would otherwise have run from save_post — so the data shows up on the frontend and in admin UI immediately.

File Structure

simple-export-import/
├── simple-export-import.php          # Bootstrap (constants, requires, init)
├── uninstall.php                     # Cleans up options on plugin delete
├── README.md
├── .gitignore
├── includes/
│   ├── helpers.php                   # Defaults, capabilities, validation, multilingual detection
│   ├── class-sei-multilingual.php    # WPML / WP-LOC compatibility layer
│   ├── class-sei-media.php           # Base64 embed + ID/URL remap + per-language attachment translations
│   ├── class-sei-settings.php        # Settings page, option registration
│   ├── class-sei-export.php          # JSON export + post row actions
│   ├── class-sei-import.php          # Tools → Import Post page + $wpdb-direct insert
│   └── class-sei-github-updater.php  # WP admin auto-update from GitHub master branch
└── languages/
    ├── simple-export-import.pot          # Translation template
    ├── simple-export-import-uk.po/.mo    # Ukrainian
    ├── simple-export-import-de_DE.po/.mo # German
    └── simple-export-import-ru_RU.po/.mo # Russian

No Composer / external dependencies — plain require_once.

Installation

One-time setup

  1. Upload the simple-export-import folder to /wp-content/plugins/ (or install the zip via Plugins → Add New → Upload).
  2. Activate via Plugins menu.
  3. Configure under Settings → Export & Import.

Updates

Once installed, updates ship through the regular WordPress Updates screen — no FTP, no zip uploads. The plugin watches the master branch of its GitHub repository and surfaces a "new version available" notice when the header version changes. One-click Update now does the rest.

See GitHub auto-update below for details.

Settings

The settings page is grouped into six sections.

General

  • Post Types — which post types get the Export row action.

Permissions

  • Export Capability — minimum capability required to export.
  • Import Capability — minimum capability required to import.

Import Behavior

  • Import Status — default post status for imported posts (Draft / Published / Pending / Private).
  • Import Title Suffix — text appended to imported titles. Default - imported; leave empty to keep the original title unchanged.
  • Preserve Original Author — when enabled and the original author's user ID exists on this site, they are kept as the author; otherwise the current user becomes the author.

Export Behavior

  • Pretty-print JSON — toggle indentation (off = smaller files).
  • Extra Meta Keys to Skip — additional meta keys to exclude from export, one per line. WordPress internals (_edit_lock, _wp_trash_meta_status, etc.) are already skipped.
  • Max Upload Size (MB) — limit on uploaded JSON file size at import time. Default and ceiling are derived from the server's PHP config (min(upload_max_filesize, post_max_size) via wp_max_upload_size()). The setting can only tighten the cap, never raise it past what PHP physically accepts. The current PHP cap is shown in the field's description.

Media

  • Embed Media Files — when on, every attachment referenced by the post (featured image, attached media, Gutenberg/ACF blocks, post meta including nested ACF Repeater/Group/Flexible) is embedded as base64 in the JSON. On import, each is recreated on the target site and all references are remapped from old IDs to new IDs across post_content (via parse_blocks/serialize_blocks), block attributes, meta fields, and wp-image-N / data-id / [gallery ids=""] legacy markup. URLs are remapped too. Multilingual-aware: the same image referenced across translation posts is uploaded once and all language posts get the same new ID. WPML Media Translation (per-language metadata on one file) round-trips: one file is written to uploads/, and N sibling attachment rows share _wp_attached_file / _wp_attachment_metadata while carrying per-language title/alt/caption/description; they are connected via wpml_set_element_language_details with element_type=post_attachment.
  • Max Embedded File Size (KB) — files above this limit are exported as references only (no embed). Default 10240 (10 MB). The current server memory_limit is shown in the field's description so you can pick a value that won't OOM during base64 decode (which inflates files by ~33%).

Multilingual

  • Multilingual Translations — when active, exporting any post bundles all of its translations into the same JSON. On import, translations are recreated and re-connected via the multilingual plugin. Auto-disabled if no compatible plugin is detected.

Usage

Export

  1. Open any post list (Posts, Pages, or a custom post type enabled in Settings).
  2. Hover a row → click Export under the row actions.
  3. A JSON file downloads with everything needed to recreate the post.

Import

  1. Go to Tools → Import Post.
  2. Pick a JSON file produced by this plugin.
  3. Click Import Post.
  4. A success notice with a link to the new post appears.

Multilingual support (WPML & WP-LOC)

The plugin talks to multilingual backends through the WPML hook surface only: wpml_element_language_details, wpml_get_element_translations, wpml_element_trid, wpml_set_element_language_details. This means it works transparently with:

  • WPML itself
  • WP-LOC (lightweight multilingual plugin that emulates the WPML hooks)
  • Any other plugin registering the same hooks

Detection is automatic (sei_is_multilingual_active() in includes/helpers.php). Language codes round-trip in WPML format (en, uk, de...) regardless of the underlying plugin's internal representation.

Round-trip matrix

Export from Import to Posts Attachments (with embed enabled)
WPML WPML ✅ connected ✅ per-language siblings connected
WPML WP-LOC ✅ connected ✅ per-language siblings connected
WP-LOC WP-LOC ✅ connected ✅ per-language siblings connected
WP-LOC WPML ✅ connected ✅ per-language siblings connected
any site without multilingual plugin ⚠️ imported as separate posts (notice shown) ⚠️ siblings imported but not linked

Yoast SEO

Yoast meta (_yoast_wpseo_*) round-trips automatically — none of those keys are in the skip-list. Since the import bypasses save_post, the plugin explicitly calls Indexable_Builder::build_for_id_and_type( $post_id, 'post' ) after meta restore so the wp_yoast_indexable cache is rebuilt and SEO tags actually render on the frontend. Done per post and per imported translation. Rank Math link cache is cleared similarly (rank_math/links/clear_cache_for_post).

What gets exported

{
  "ID": 123,
  "post_title": "Sample Post",
  "post_content": "<!-- wp:paragraph --><p>...</p><!-- /wp:paragraph -->",
  "post_excerpt": "...",
  "post_status": "publish",
  "post_name": "sample-post",
  "post_type": "post",
  "post_author": "1",
  "post_date": "2024-01-01 12:00:00",
  "post_date_gmt": "2024-01-01 12:00:00",
  "comment_status": "open",
  "ping_status": "closed",
  "menu_order": 0,
  "meta_fields": { "custom_field": "value", "_field_5e0a1b2c": "...", "_yoast_wpseo_title": "..." },
  "taxonomies": { "category": [ { "term_id": 5, "name": "News", "slug": "news" } ] },
  "featured_image": { "id": 456, "url": "...", "file": "..." },
  "attachments": [ { "id": 789, "title": "...", "url": "...", "file": "..." } ],
  "embedded_attachments": [
    {
      "id": 456,
      "filename": "hero.jpg",
      "mime_type": "image/jpeg",
      "base64": "...",
      "language_code": "en",
      "translations": [
        { "id": 457, "language_code": "uk", "title": "...", "alt": "...", "caption": "...", "description": "..." }
      ]
    }
  ],
  "wpml_enabled": true,
  "original_language": "en",
  "translations": [
    { "language_code": "uk", "post_title": "...", "post_content": "...", "meta_fields": {...}, "embedded_attachments": [...], ... }
  ]
}
  • embedded_attachments is only present when Embed Media Files is on. Without it, attachments are still referenced by ID in featured_image, attachments, and inside post_content — but the importer can't recreate them (target must already have those attachment IDs).
  • translations is only present when a multilingual plugin is active and the Multilingual Translations setting is on.

GitHub auto-update

The plugin watches the master branch of its public GitHub repository. The flow:

  1. WordPress's normal update check (twice-daily cron, or Check Again on the Updates screen with ?force-check=1) fires the pre_set_site_transient_update_plugins filter.
  2. SEI_GitHub_Updater fetches the plugin header from https://raw.githubusercontent.com/vitaliikaplia/simple-export-import/master/simple-export-import.php (12-hour cache, 1-hour on failure).
  3. If the remote Version: is greater than the installed version, an update entry is injected into the transient.
  4. The Plugins screen and Dashboard → Updates show Update now, which downloads https://github.com/vitaliikaplia/simple-export-import/archive/refs/heads/master.zip.
  5. upgrader_source_selection renames GitHub's simple-export-import-master/ extraction to simple-export-import/ so WP installs it in the right place.

No GitHub API token, no rate-limit headache — raw.githubusercontent.com and archive/refs/heads/<branch>.zip are static endpoints.

To track a different branch (e.g. a staging branch) on a particular site, define the constant in wp-config.php:

define( 'SEI_GITHUB_BRANCH', 'develop' );

Localization

Bundled translations: Ukrainian (uk), German (de_DE), Russian (ru_RU).

To add a new language:

  1. Open languages/simple-export-import.pot in Poedit or any PO editor.
  2. Save as simple-export-import-{locale}.po (e.g. simple-export-import-pl_PL.po).
  3. Compile to .mo: msgfmt -o simple-export-import-{locale}.mo simple-export-import-{locale}.po.
  4. WordPress picks the right file based on the site locale.

Requirements

  • WordPress 4.7+
  • PHP 7.4+
  • JSON support (default in PHP)

Security

  • Nonce verification on every export request and import form
  • Capability checks (configurable per action)
  • File size + extension + JSON-validity check on upload
  • All input sanitized; output escaped
  • Direct $wpdb import path intentionally bypasses save_post / add_attachment / transition_post_status and similar hooks — content is written exactly as exported. If a theme or plugin needs to react to imported posts (e.g. custom search indexing), it must hook a later admin action or re-save the post manually.

Read the full README on GitHub →