WP Manifestindependent plugin directory
manifest / seo / yoast-metadata-import-export

Yoast Metadata Import Export

WordPress plugin for import/export of Yoast SEO metadata with client-side parsing, preview, and secure application of changes.

by Kavit Trivedi, Raam Dev · github.com/trivedikavit/yoast-metadata-import-export

1stars
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/trivedikavit/yoast-metadata-import-export/archive/refs/heads/trivedikavit%2Flocal.zip

WordPress plugin to bulk export and import Yoast SEO meta for posts and terms as CSV. The Import tab parses CSV entirely in the browser, then applies changes server-side in small batches through a nonce-protected REST endpoint.

Features

  • Export Yoast SEO meta for any selection of public post types and taxonomies to a single CSV file (UTF-8 with BOM, so spreadsheet apps detect the encoding correctly).
  • Import that CSV back to update Yoast SEO meta in bulk, with three knobs:
    • Dry run — validate without writing.
    • Force — overwrite existing non-empty values.
    • Clear existing — wipe every Yoast field on the target before writing.
  • Batched imports with a live progress bar (see How it works for the request shape).
  • React + TypeScript admin UI built on @wordpress/components so it inherits the standard WordPress admin theme.

How it works (and why it's a fit for WPVIP)

The plugin never uploads your CSV as a file — parsing happens in the browser, and only small JSON batches cross the network. That's what makes it safe for very large imports and a natural fit for platforms with a read-only application filesystem like WPVIP.

Importing: the file never leaves your browser

  1. You drop a CSV on the upload zone in the Import tab.
  2. The browser reads the file into memory with FileReader.readAsText() — no network request happens at this point.
  3. parseCsv() tokenises and validates rows locally in the browser.
  4. The admin app slices the parsed rows into batches of 100 and POSTs each batch as JSON to /ymie/v1/import-batch, with a 1-second pause between requests.
  5. PHP applies each batch with update_post_meta() / update_term_meta() and returns a count.

Nothing else moves over the wire. There is no multipart/form-data upload, no temp file in /tmp, no entry in wp-content/uploads/. The raw CSV exists only inside the browser tab — PHP never sees it.

Exporting: the CSV is streamed, not stored

  1. The Export tab POSTs your post-type/taxonomy selection to /ymie/v1/export.
  2. The handler writes rows straight to php://output (with a UTF-8 BOM), row by row.
  3. The browser receives a Content-Type: text/csv response and downloads it.

The server never writes the export to disk. There is no temp file, no upload to S3, no intermediate artifact — the HTTP response is the CSV.

Why this is great on read-only / locked-down platforms

WPVIP, Pantheon, WP Engine and Kinsta run application code on read-only filesystems (or ephemeral containers where any local write disappears at the next deploy). They also tend to enforce strict PHP limits:

Constraint Typical managed-host value What it breaks for naive import plugins
upload_max_filesize 50–100 MB Large CSVs are rejected before PHP ever runs
post_max_size 100–128 MB Same, but for the POST body
max_execution_time 30–120 s One long import request times out and leaves it half-done
Writable local disk None (uploads bucket only) move_uploaded_file(), tempnam(), fwrite to /tmp all fail

Because this plugin never needs the local filesystem and never sends a large payload in a single request, none of those limits apply:

  • No upload_max_filesize ceiling. A 500 MB CSV with hundreds of thousands of rows is the browser's problem. The server only ever sees a small JSON blob containing 100 parsed rows — a few tens of KB per request.
  • No disk writes anywhere. Imports call update_*_meta(); exports stream to php://output. Nothing touches wp-content/uploads/, /tmp, or sys_get_temp_dir(), so the read-only filesystem is never a problem.
  • No long-running request. Each batch (100 rows) finishes in well under a second on typical hardware. A 100,000-row import is 1,000 short, independent HTTP calls, so per-request execution time stays well under max_execution_time regardless of dataset size. The 1-second pause between batches also keeps the import from tripping WAF rate limits.
  • Resilient under failure. A failed batch is surfaced in the response and the next batch continues — you don't lose successful writes from earlier in the import when a later batch errors.

Requirements

  • WordPress ≥ 6.0
  • PHP ≥ 7.4
  • Yoast SEO installed and active
  • Node ≥ 18 and Composer 2.x for development

The plugin self-deactivates with an admin notice if Yoast SEO is not active.

Installation (development)

git clone <repo> yoast-metadata-import-export
cd yoast-metadata-import-export
composer install   # PHPUnit + WordPress Coding Standards
npm install        # React/TypeScript toolchain
npm run build      # produces build/index.js and build/style-index.css

Symlink (or copy) the directory into your wp-content/plugins/ and activate "Yoast Metadata Import Export" from the WordPress admin. The "Yoast Meta" menu entry will appear in the sidebar.

Development scripts

JavaScript / TypeScript

npm run start    # development build with watch
npm run build    # production build
npm test         # Jest unit tests (wp-scripts test-unit-js)
npm run lint     # ESLint (src/)
npm run format   # Prettier (src/)
npx tsc --noEmit # standalone TypeScript type-check

Build output goes to build/, which is committed — the plugin loads compiled JS/CSS from there at runtime and must work on a host without a Node toolchain. Re-run npm run build and commit the result whenever src/ changes. The PHP enqueue falls back to assets/css/admin.css only when the build is missing.

PHP

composer install      # install dev tooling (PHPUnit, PHPCS, WPCS)
vendor/bin/phpcs      # WordPress coding standards check
vendor/bin/phpunit    # unit tests (no WordPress install required)

The PHPUnit suite runs against tests/bootstrap.php, which stubs every WordPress function the plugin touches in-memory — there is no need for a wp-tests-lib environment.

Architecture

yoast-metadata-import-export.php   Plugin entry: constants, hooks, Yoast dependency check
includes/
  admin/class-ymie-admin.php       Admin menu, asset enqueue, page render
  api/class-ymie-rest.php          REST routes (POST ymie/v1/export, ymie/v1/import-batch)
src/                               TypeScript source (wp-scripts entry)
  index.tsx                        React entry — mounts AdminApp on #ymie-root
  admin-app.tsx                    Tabbed Export/Import UI
  components/FileUpload.tsx        Drag-and-drop CSV upload
  csv-parser.ts                    Quote-aware CSV parser + row validator
  api.ts                           apiFetch + raw-fetch wrappers around the REST routes
  types.ts                         Shared TypeScript types + ymieSettings global
  globals.d.ts                     CSS side-effect import declaration
  style.css                        Admin app styles (bundled to build/style-index.css)
  __tests__/csv-parser.test.ts     Jest unit tests
assets/css/admin.css               Fallback stylesheet used only when build/ is missing
build/                             Compiled JS/CSS — committed; rebuild with `npm run build`
samples/sample.csv                 Example import file matching the export schema
tests/
  bootstrap.php                    PHPUnit bootstrap with WP function/REST stubs
  test-ymie.php                    Unit tests for YMIE_REST and YMIE_Admin wiring

REST API

Both routes live under the ymie/v1 namespace and require the manage_options capability plus the standard REST nonce (X-WP-Nonce header, created with action wp_rest).

POST /wp-json/ymie/v1/export

Streams a CSV download. Request body:

{
  "postTypes":  ["post", "page"],
  "taxonomies": ["category", "post_tag"]
}

Response: Content-Type: text/csv; charset=utf-8 with Content-Disposition: attachment; filename="yoast-meta-export-YYYY-MM-DD.csv". The first four columns are ID, Type, Type_Value, Title/Name; the remaining columns are the 23 Yoast meta keys listed in YMIE_REST::$yoast_fields.

POST /wp-json/ymie/v1/import-batch

Applies a batch of rows. Request body:

{
  "batch": [
    { "id": 101, "type": "post", "_yoast_wpseo_title": "New title", ... }
  ],
  "options": {
    "dryRun":        false,
    "force":         false,
    "clearExisting": false
  }
}

Response:

{
  "success": true,
  "updated": 1,
  "errors":  [],
  "dry_run": false,
  "options": { "dryRun": false, "force": false, "clearExisting": false }
}

Behaviour:

  • id must be a positive integer; type must be "post" or "term". Rows failing either check land in errors.
  • Only keys that appear in YMIE_REST::$yoast_fields are read from each row; extras are ignored.
  • Without force, an existing non-empty Yoast value is preserved when the row would replace it (the row contributes nothing to updated).
  • clearExisting deletes every Yoast field on the target before writes; unrelated post meta is untouched.
  • During dryRun, no update_*_meta / delete_*_meta calls are made, but the row is still counted in updated so the client can preview the change set.

Supported Yoast fields

The 23 keys exported and imported by the plugin live in YMIE_REST::$yoast_fields:

Category Keys
Core SEO _yoast_wpseo_title, _yoast_wpseo_metadesc, _yoast_wpseo_focuskw, _yoast_wpseo_keywordsynonyms, _yoast_wpseo_focuskeywords, _yoast_wpseo_canonical, _yoast_wpseo_bctitle
Robots _yoast_wpseo_meta-robots-noindex, _yoast_wpseo_meta-robots-nofollow, _yoast_wpseo_meta-robots-adv
Advanced _yoast_wpseo_redirect, _yoast_wpseo_is_cornerstone, _yoast_wpseo_primary_category
Open Graph _yoast_wpseo_opengraph-title, _yoast_wpseo_opengraph-description, _yoast_wpseo_opengraph-image, _yoast_wpseo_opengraph-image-id
Twitter _yoast_wpseo_twitter-title, _yoast_wpseo_twitter-description, _yoast_wpseo_twitter-image, _yoast_wpseo_twitter-image-id
Schema _yoast_wpseo_schema_page_type, _yoast_wpseo_schema_article_type

A working CSV with the exact column order is in samples/sample.csv.

Security

  • Admin page and both REST routes require the manage_options capability.
  • REST requests must carry a valid wp_rest nonce; the React client sends it via apiFetch's nonce middleware and via the raw X-WP-Nonce header on the export download.
  • File parsing happens entirely in the browser — the CSV body itself is never transmitted; only the parsed, validated row objects are POSTed.
  • Every imported field passes through sanitize_text_field() before update_post_meta() / update_term_meta().

License

GPL v2 or later.