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
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.zipReadme
=== 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_contentfilter prior to export, allowing third-party plugins to modify the output) - Converts all
post_contentto Markdown - Converts all
post_metaand fields within thewp_poststable to YAML front matter for parsing by Jekyll - Generates a
_config.ymlwith all settings in thewp_optionstable - Outputs a single zip file with
_config.yml, pages, and_postsfolder containing.mdfiles 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 ==
- Place plugin in
/wp-content/plugins/folder - Activate plugin in WordPress dashboard
- Select
Export to Jekyllfrom theToolsmenu
== More information ==
- Changelog
- Command-line-usage
- Selective export by category or tag
- Custom post types
- Custom fields
- Developing locally
- Minimum required PHP version
=== 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 =
- Go to Posts > Categories or Posts > Tags
- Hover over the category/tag name
- Look at the browser's status bar or the URL - you'll see something like
tag_ID=123&taxonomy=post_tag&term_slug=featured - 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:
- Check the slug: Make sure you're using the term slug, not the name
- Use
wp term list categoryto verify the exact slug
- Use
- Check post status: Only published, future, and draft posts are exported
- Verify taxonomy: Make sure you're using the correct taxonomy name (
category,post_tag, etc.)
= Wrong Posts Exported =
If you're getting unexpected posts:
- Check term associations: Verify which posts have the category/tag assigned
- Review filter logic: Remember that multiple categories use OR logic
- Clear cache: If testing, use
wp cache flushbetween 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
revisionwas 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 thejekyll_export_post_typesfilter - Broadened the
convert_content()fallback to catch anyThrowable(not justInvalidArgumentException) 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 inColspanTableConverter
= 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 hitmemory_limitafter a successful build zip_folder()now throwsRuntimeExceptioninstead of callingwp_die()directly, so the existingexport()try/catch renders a friendly error and runscleanup()on partial temp files- Added
jekyll_export_html_converterfilter so integrations (and tests) can swap in a custom HTML-to-Markdown converter - Gated the v4.0.3 fallback
error_log()call behindWP_DEBUG - Hardened sanitization of
$_GET['type']in the export callback - Added regression tests for the v4.0.3
Invalid HTML was providedfallback and for the newzip_folder()throw behavior
= 4.0.3 =
- Catch
InvalidArgumentExceptionfromleague/html-to-markdowninconvert_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_limitpre-flight check (warns when below 64MB) invalidate_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 ofmd5(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.phpbefore bootstrapping WordPress (CWE-665) - Security: Sanitize each path segment of page filenames as defense-in-depth against path traversal (CWE-22)
- Fix stale
$upload_basedircache incopy_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/yamlfrom ^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.phpin favor oflib/cli.php - Improved CI pipeline with PHPStan job and vendor consistency checks
== Developing locally ==
= Option 1: Using Dev Containers (Recommended) =
The easiest way to get started is using VS Code Dev Containers or GitHub Codespaces:
- Install VS Code and the Dev Containers extension
git clone https://github.com/benbalter/wordpress-to-jekyll-exporter- Open the folder in VS Code
- Click "Reopen in Container" when prompted
- Wait for the container to build and dependencies to install
- 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 =
sudo apt-get updatesudo apt-get install composersudo apt-get install php7.3-xmlsudo apt-get install php7.3-mysqlsudo apt-get install php7.3-zipsudo apt-get install php-mbstringsudo apt-get install subversionsudo apt-get install mysql-serversudo apt-get install php-pearsudo pear install PHP_CodeSniffer
= Bootstrap & Setup =
git clone https://github.com/benbalter/wordpress-to-jekyll-exportercd wordpress-to-jekyll-exporterscript/bootstrapscript/setup
= Option 3: Docker Compose Only =
git clone https://github.com/benbalter/wordpress-to-jekyll-exporterdocker-compose upopen 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