WP Manifestindependent plugin directory
manifest / unclassified / wordpress-to-jekyll-exporter

Static Site Exporter

One-click WordPress plugin that converts all posts, pages, taxonomies, metadata, and settings to Markdown and YAML which can be dropped into Jekyll (or Hugo or any other Markdown and YAML based site engine).

by Ben Balter · github.com/benbalter/wordpress-to-jekyll-exporter · website

1.1kstars
128forks

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/benbalter/wordpress-to-jekyll-exporter/archive/refs/heads/master.zip

Readme

=== Static Site Exporter === Contributors: benbalter Tags: jekyll, github, github pages, yaml, export, markdown Requires at least: 6.4 Tested up to: 6.9 Requires PHP: 8.2 Stable tag: 4.1.0 License: GPLv3 or later License URI: http://www.gnu.org/licenses/gpl-3.0.html GitHub Plugin URI: benbalter/wordpress-to-jekyll-exporter Primary Branch: master == Features ==

  • Converts all posts, pages, and settings from WordPress to Markdown and YAML for use in Jekyll (or Hugo or any other Markdown and YAML based site engine)
  • Export what your users see, not what the database stores (runs post content through the_content filter prior to export, allowing third-party plugins to modify the output)
  • Converts all post_content to Markdown
  • Converts all post_meta and fields within the wp_posts table to YAML front matter for parsing by Jekyll
  • Generates a _config.yml with all settings in the wp_options table
  • Outputs a single zip file with _config.yml, pages, and _posts folder containing .md files for each post in the proper Jekyll naming convention
  • Selective export: Export only specific categories, tags, or post types using WP-CLI
  • No settings. Just a single click.

== Usage ==

  1. Place plugin in /wp-content/plugins/ folder
  2. Activate plugin in WordPress dashboard
  3. Select Export to Jekyll from the Tools menu

== More information ==

See the full documentation:

=== Selective Export by Category or Tag ===

This feature allows you to export only a specific subset of your WordPress content, filtered by category, tag, or post type. This is particularly useful when:

  • You have a large WordPress site but only need to convert specific sections
  • You want to migrate content by topic or category
  • You need to export content incrementally

== Using WP-CLI ==

The easiest way to perform selective exports is via WP-CLI commands.

= Export by Category =

To export posts from a single category, use the category slug:

wp jekyll-export --category=technology > technology-export.zip

To export from multiple categories (OR logic - posts in any of these categories):

wp jekyll-export --category=tech,news,updates > export.zip

= Export by Tag =

To export posts with a specific tag:

wp jekyll-export --tag=featured > featured-export.zip

To export posts with multiple tags (OR logic):

wp jekyll-export --tag=featured,popular > export.zip

= Export Specific Post Types =

To export only pages:

wp jekyll-export --post_type=page > pages-export.zip

To export only posts:

wp jekyll-export --post_type=post > posts-export.zip

To export custom post types:

wp jekyll-export --post_type=portfolio,testimonial > custom-export.zip

= Combining Filters =

You can combine multiple filters. Posts must match ALL specified filters (AND logic):

=== Export posts that are in "technology" category AND have "featured" tag ===
wp jekyll-export --category=technology --tag=featured --post_type=post > export.zip

== Using PHP Filters ==

For more programmatic control, you can use WordPress filters directly in your theme's functions.php or a custom plugin.

= Filter by Category =

add_filter( 'jekyll_export_taxonomy_filters', function() {
    return array(
        'category' => array( 'technology', 'science' ),
    );
} );

= Filter by Tag =

add_filter( 'jekyll_export_taxonomy_filters', function() {
    return array(
        'post_tag' => array( 'featured', 'popular' ),
    );
} );

= Filter by Custom Taxonomy =

add_filter( 'jekyll_export_taxonomy_filters', function() {
    return array(
        'my_custom_taxonomy' => array( 'term-slug-1', 'term-slug-2' ),
    );
} );

= Combine Multiple Taxonomies =

add_filter( 'jekyll_export_taxonomy_filters', function() {
    return array(
        'category' => array( 'technology' ),
        'post_tag' => array( 'featured' ),
        'custom_tax' => array( 'term-1' ),
    );
} );

= Filter Post Types =

add_filter( 'jekyll_export_post_types', function() {
    return array( 'post', 'page' ); // Only export posts and pages
} );

== Finding Category and Tag Slugs ==

If you're not sure what slug to use:

= Via WordPress Admin =

  1. Go to Posts > Categories or Posts > Tags
  2. Hover over the category/tag name
  3. Look at the browser's status bar or the URL - you'll see something like tag_ID=123&taxonomy=post_tag&term_slug=featured
  4. The slug is the part after term_slug=

= Via WP-CLI =

List all categories with their slugs:

wp term list category --fields=name,slug

List all tags with their slugs:

wp term list post_tag --fields=name,slug

== Use Cases ==

= Scenario 1: Export a Single Blog Section =

You have a WordPress site with multiple sections (Tech, Lifestyle, Travel) and want to move just the Tech section to a static site:

wp jekyll-export --category=tech > tech-blog-export.zip

= Scenario 2: Export Featured Content =

You want to export only posts marked as "featured" for a special showcase site:

wp jekyll-export --tag=featured > featured-content.zip

= Scenario 3: Export by Year (using custom taxonomy) =

If you've tagged posts by year, you can export by year:

wp jekyll-export --tag=2024 > 2024-posts.zip

= Scenario 4: Migrate Content Incrementally =

Export different categories separately for incremental migration:

wp jekyll-export --category=tech > tech.zip
wp jekyll-export --category=news > news.zip
wp jekyll-export --category=reviews > reviews.zip

== Technical Details ==

  • Taxonomy Filtering: Uses WordPress term slugs (not names or IDs)
  • Query Performance: Filtering is done at the database level for efficiency
  • OR Logic Within Taxonomy: Multiple terms in the same taxonomy use OR logic (e.g., posts in category A OR B)
  • AND Logic Across Taxonomies: Multiple taxonomies use AND logic (e.g., posts in category A AND having tag B)
  • Post Type Filtering: Works independently of taxonomy filtering

== Limitations ==

  • Revisions are excluded when using taxonomy filters (as they don't have taxonomy terms)
  • Taxonomy filtering uses term slugs, not term IDs or names
  • Empty taxonomy filters are ignored (no filtering applied)

== Troubleshooting ==

= No Posts Exported =

If your export is empty:

  1. Check the slug: Make sure you're using the term slug, not the name
    • Use wp term list category to verify the exact slug
  2. Check post status: Only published, future, and draft posts are exported
  3. Verify taxonomy: Make sure you're using the correct taxonomy name (category, post_tag, etc.)

= Wrong Posts Exported =

If you're getting unexpected posts:

  1. Check term associations: Verify which posts have the category/tag assigned
  2. Review filter logic: Remember that multiple categories use OR logic
  3. Clear cache: If testing, use wp cache flush between exports

== Custom post types ==

To export custom post types, you'll need to add a filter (w.g. to your themes config file) to do the following:

add_filter( 'jekyll_export_post_types', function() {
    return array('post', 'page', 'you-custom-post-type');
});

The custom post type will be exported as a Jekyll collection. You'll need to initialize it in the resulting Jekyll site's _config.yml.

== Changelog ==

= 4.1.0 =

  • Behavior change: Post revisions are no longer exported by default. Previously revision was included in the default post types, which filled the _drafts/ folder with duplicate copies of every post. To restore the old behavior, re-add 'revision' via the jekyll_export_post_types filter
  • Broadened the convert_content() fallback to catch any Throwable (not just InvalidArgumentException) from the HTML-to-Markdown converter, so an unexpected converter error falls back to the post's raw HTML instead of aborting the entire export
  • Emit a WP_DEBUG-gated warning when a public custom field shadows a reserved front matter key (e.g. layout, image, date), surfacing silent overrides. The override behavior itself is unchanged
  • Internal: de-duplicated the raw-HTML fallback filters in convert_content() and the reflection boilerplate in ColspanTableConverter

= 4.0.4 =

  • Stream the export zip to the browser in 8 KB chunks instead of loading the entire archive into memory in send(), so large exports no longer hit memory_limit after a successful build
  • zip_folder() now throws RuntimeException instead of calling wp_die() directly, so the existing export() try/catch renders a friendly error and runs cleanup() on partial temp files
  • Added jekyll_export_html_converter filter so integrations (and tests) can swap in a custom HTML-to-Markdown converter
  • Gated the v4.0.3 fallback error_log() call behind WP_DEBUG
  • Hardened sanitization of $_GET['type'] in the export callback
  • Added regression tests for the v4.0.3 Invalid HTML was provided fallback and for the new zip_folder() throw behavior

= 4.0.3 =

  • Catch InvalidArgumentException from league/html-to-markdown in convert_content() and fall back to the post's raw HTML for that single post instead of aborting the entire export with "Jekyll Export failed: Invalid HTML was provided" (#400)

= 4.0.2 =

  • Add shutdown handler to surface fatal errors (memory exhaustion, max execution time) during export with actionable error messages instead of a generic WordPress critical error page
  • Add proactive memory_limit pre-flight check (warns when below 64MB) in validate_environment()
  • Display admin error notice on Tools → Export when environment validation fails, before the user clicks Export

= 4.0.1 =

  • Security: Use cryptographically secure randomness (wp_generate_password) instead of md5(time()) for the export temp directory name to prevent symlink/TOCTOU attacks on shared hosts (CWE-330/377)
  • Security: Reject non-CLI access in deprecated jekyll-export-cli.php before bootstrapping WordPress (CWE-665)
  • Security: Sanitize each path segment of page filenames as defense-in-depth against path traversal (CWE-22)
  • Fix stale $upload_basedir cache in copy_recursive() on multisite by keying it on the current blog ID

= 4.0.0 =

  • Breaking: Minimum PHP version bumped from 7.2.5 to 8.2
  • Breaking: Minimum WordPress version bumped from 4.4 to 6.4
  • Updated symfony/yaml from ^5.4 to ^7.0
  • Updated PHPUnit from ~8.0 to ~9.6
  • Removed symfony/polyfill-php80 (no longer needed)
  • Added PHPStan static analysis at level 5
  • Fixed get_posts() to return integer IDs instead of strings
  • Fixed PHPDoc type annotations throughout codebase
  • Deprecated legacy jekyll-export-cli.php in favor of lib/cli.php
  • Improved CI pipeline with PHPStan job and vendor consistency checks

View Past Releases

== Developing locally ==

= Option 1: Using Dev Containers (Recommended) =

The easiest way to get started is using VS Code Dev Containers or GitHub Codespaces:

  1. Install VS Code and the Dev Containers extension
  2. git clone https://github.com/benbalter/wordpress-to-jekyll-exporter
  3. Open the folder in VS Code
  4. Click "Reopen in Container" when prompted
  5. Wait for the container to build and dependencies to install
  6. Access WordPress at http://localhost:8088

The devcontainer includes:

  • Pre-configured WordPress and MySQL
  • All PHP extensions and Composer dependencies
  • VS Code extensions for PHP development, debugging, and testing
  • WordPress coding standards configured

See .devcontainer/README.md for more details.

= Option 2: Manual Setup =

= Prerequisites =

  1. sudo apt-get update
  2. sudo apt-get install composer
  3. sudo apt-get install php7.3-xml
  4. sudo apt-get install php7.3-mysql
  5. sudo apt-get install php7.3-zip
  6. sudo apt-get install php-mbstring
  7. sudo apt-get install subversion
  8. sudo apt-get install mysql-server
  9. sudo apt-get install php-pear
  10. sudo pear install PHP_CodeSniffer

= Bootstrap & Setup =

  1. git clone https://github.com/benbalter/wordpress-to-jekyll-exporter
  2. cd wordpress-to-jekyll-exporter
  3. script/bootstrap
  4. script/setup

= Option 3: Docker Compose Only =

  1. git clone https://github.com/benbalter/wordpress-to-jekyll-exporter
  2. docker-compose up
  3. open localhost:8088

== Running tests ==

script/cibuild

== Custom fields ==

When using custom fields (e.g. with the Advanced Custom fields plugin) you might have to register a filter to convert array style configs to plain values.

= Available Filters =

The plugin provides two filters for customizing post metadata:

  • jekyll_export_meta: Filters the metadata for a single post before it's merged with taxonomy terms. Receive

Read the full README on GitHub →