
the_content filter prior to export, allowing third-party plugins to modify the output)post_content to Markdownpost_meta and fields within the wp_posts table to YAML front matter for parsing by Jekyll_config.yml with all settings in the wp_options table_config.yml, pages, and _posts folder containing .md files for each post in the proper Jekyll naming convention/wp-content/plugins/ folderExport to Jekyll from the Tools menuThis 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:
The easiest way to perform selective exports is via WP-CLI commands.
To export posts from a single category, use the category slug:
`bash
wp jekyll-export –category=technology > technology-export.zip
`
To export from multiple categories (OR logic – posts in any of these categories):
`bash
wp jekyll-export –category=tech,news,updates > export.zip
`
To export posts with a specific tag:
`bash
wp jekyll-export –tag=featured > featured-export.zip
`
To export posts with multiple tags (OR logic):
`bash
wp jekyll-export –tag=featured,popular > export.zip
`
To export only pages:
`bash
wp jekyll-export –post_type=page > pages-export.zip
`
To export only posts:
`bash
wp jekyll-export –post_type=post > posts-export.zip
`
To export custom post types:
`bash
wp jekyll-export –post_type=portfolio,testimonial > custom-export.zip
`
You can combine multiple filters. Posts must match ALL specified filters (AND logic):
`bash<h3>Export posts that are in "technology" category AND have "featured" tag</h3>wp jekyll-export --category=technology --tag=featured --post_type=post > export.zip
`<h3>Using PHP Filters</h3>
For more programmatic control, you can use WordPress filters directly in your theme’s functions.php or a custom plugin.
`php
add_filter( ‘jekyll_export_taxonomy_filters’, function() {
return array(
‘category’ => array( ‘technology’, ‘science’ ),
);
} );
`
`php
add_filter( ‘jekyll_export_taxonomy_filters’, function() {
return array(
‘post_tag’ => array( ‘featured’, ‘popular’ ),
);
} );
`
`php
add_filter( ‘jekyll_export_taxonomy_filters’, function() {
return array(
‘my_custom_taxonomy’ => array( ‘term-slug-1’, ‘term-slug-2’ ),
);
} );
`
`php
add_filter( ‘jekyll_export_taxonomy_filters’, function() {
return array(
‘category’ => array( ‘technology’ ),
‘post_tag’ => array( ‘featured’ ),
‘custom_tax’ => array( ‘term-1’ ),
);
} );
`
`php
add_filter( ‘jekyll_export_post_types’, function() {
return array( ‘post’, ‘page’ ); // Only export posts and pages
} );
`
If you’re not sure what slug to use:
tag_ID=123&taxonomy=post_tag&term_slug=featuredterm_slug=List all categories with their slugs:
`bash
wp term list category –fields=name,slug
`
List all tags with their slugs:
`bash
wp term list post_tag –fields=name,slug
`
You have a WordPress site with multiple sections (Tech, Lifestyle, Travel) and want to move just the Tech section to a static site:
`bash
wp jekyll-export –category=tech > tech-blog-export.zip
`
You want to export only posts marked as “featured” for a special showcase site:
`bash
wp jekyll-export –tag=featured > featured-content.zip
`
If you’ve tagged posts by year, you can export by year:
`bash
wp jekyll-export –tag=2024 > 2024-posts.zip
`
Export different categories separately for incremental migration:
`bash
wp jekyll-export –category=tech > tech.zip
wp jekyll-export –category=news > news.zip
wp jekyll-export –category=reviews > reviews.zip
`
If your export is empty:
wp term list category to verify the exact slugcategory, post_tag, etc.)If you’re getting unexpected posts:
wp cache flush between exportsTo export custom post types, you’ll need to add a filter (w.g. to your themes config file) to do the following:
`php
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.
The easiest way to get started is using VS Code Dev Containers or GitHub Codespaces:
git clone https://github.com/benbalter/wordpress-to-jekyll-exporterhttp://localhost:8088The 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.
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_CodeSniffergit clone https://github.com/benbalter/wordpress-to-jekyll-exportercd wordpress-to-jekyll-exporterscript/bootstrapscript/setupgit clone https://github.com/benbalter/wordpress-to-jekyll-exporterdocker-compose upopen localhost:8088script/cibuild<h3>Custom fields</h3>
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.
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. Receives $meta array as the only parameter.jekyll_export_post_meta: Filters the complete metadata array (including taxonomy terms) just before it’s written to the YAML frontmatter. Receives $meta array and $post object as parameters. This is the recommended filter for most use cases.Note: As of the latest version, the plugin no longer automatically removes empty or falsy values from the frontmatter. All metadata is preserved by default. If you want to remove certain fields, you can use the jekyll_export_post_meta filter to customize this behavior.
By default, the plugin saves custom fields in an array structure that is exported as:
`php
[“my-bool”]=>
array(1) {
[0] => string(1) “1”
}
[“location”]=>
array(1) {
[0] => string(88) “My address”
}
`
And this leads to a YAML structure like:
`yaml
my-bool:
– “1”
location:
– ‘My address’
`
This is likely not the structure you expect or want to work with. You can convert it using a filter:
`php
add_filter( ‘jekyll_export_meta’, function($meta) {
foreach ($meta as $key => $value) {
if (is_array($value) && count($value) === 1 && array_key_exists(0, $value)) {
$meta[$key] = $value[0];
}
}
return $meta;
});
`
A more complete solution could look like that:
`php
add_filter( ‘jekyll_export_meta’, function($meta) {
foreach ($meta as $key => $value) {
// Advanced Custom Fields
if (is_array($value) && count($value) === 1 && array_key_exists(0, $value)) {
$value = maybe_unserialize($value[0]);
// Advanced Custom Fields: NextGEN Gallery Field add-on
if (is_array($value) && count($value) === 1 && array_key_exists(0, $value)) {
$value = $value[0];
}
}
// convert types
$value = match ($key) {
// Advanced Custom Fields: “true_false” type
‘my-bool’ => (bool) $value,
default => $value
};
$meta[$key] = $value;
}
return $meta;
});
`
If you want to remove empty or falsy values from the frontmatter (similar to the pre-3.0.3 behavior), you can use the jekyll_export_post_meta filter:
`php
add_filter( ‘jekyll_export_post_meta’, function( $meta, $post ) {
foreach ( $meta as $key => $value ) {
// Remove falsy values except numeric 0
if ( ! is_numeric( $value ) && ! $value ) {
unset( $meta[ $key ] );
}
}
return $meta;
}, 10, 2 );
`
If you’re having trouble with your web server timing out before the export is complete, or if you just like terminal better, you may enjoy the command-line tool.
It works just like the plugin, but produces the zipfile on STDOUT:
`
php jekyll-export-cli.php > jekyll-export.zip
`
If using this method, you must run first cd into the wordpress-to-jekyll-exporter directory.
Alternatively, if you have WP-CLI installed, you can run:
`
wp jekyll-export > export.zip
`
The WP-CLI version will provide greater compatibility for alternate WordPress environments, such as when wp-content isn’t in the usual location.
You can export only specific categories or tags using the WP-CLI command. This is useful when you want to convert just one section of your WordPress site instead of the entire corpus.
`bash
wp jekyll-export –category=technology > export.zip
`
`bash
wp jekyll-export –category=tech,news,updates > export.zip
`
`bash
wp jekyll-export –tag=featured > export.zip
`
`bash
wp jekyll-export –post_type=page > export.zip
`
`bash
wp jekyll-export –category=technology –tag=featured –post_type=post > export.zip
<h3>Using Filters in PHP</h3>jekyll_export_taxonomy_filters` filter:
If you're using the plugin via PHP code or want more control, you can use the
`php
add_filter( ‘jekyll_export_taxonomy_filters’, function() {
return array(
‘category’ => array( ‘technology’, ‘science’ ),
‘post_tag’ => array( ‘featured’ ),
);
} );
// Then trigger the export
global $jekyll_export;
$jekyll_export->export();
`
This document describes the performance optimizations implemented in Static Site Exporter to improve export speed and reduce resource usage, especially for large WordPress sites.
The following optimizations have been implemented to address performance bottlenecks identified in the export process:
Problem: The original get_posts() method executed a separate SQL query for each post type, then merged the results using array_merge().
`php
// Before (inefficient)
foreach ( $post_types as $post_type ) {
$ids = $wpdb->get_col( $wpdb->prepare( “SELECT ID FROM {$wpdb->posts} WHERE post_type = %s”, $post_type ) );
$posts = array_merge( $posts, $ids );
}
`
Solution: Changed to a single SQL query using an IN clause.
`php
// After (optimized)
$placeholders = implode( ‘, ‘, array_fill( 0, count( $post_types ), ‘%s’ ) );
$query = “SELECT ID FROM {$wpdb->posts} WHERE post_type IN ($placeholders)”;
$posts = $wpdb->get_col( $wpdb->prepare( $query, $post_types ) );
`
Impact: Reduces database round trips from N (number of post types, typically 3) to 1, significantly improving performance on sites with many posts.
Problem: The convert_meta() method called get_userdata() for every post, resulting in redundant database queries for posts by the same author (N+1 query problem).
`php
// Before (inefficient)
‘author’ => get_userdata( $post->post_author )->display_name,
`
Solution: Implemented a static cache to store user data across post conversions.
`php
// After (optimized)
static $user_cache = array();
if ( ! isset( $user_cache[ $post->post_author ] ) ) {
$user_data = get_userdata( $post->post_author );
$user_cache[ $post->post_author ] = $user_data ? $user_data->display_name : ”;
}
‘author’ => $user_cache[ $post->post_author ],
`
Impact: Eliminates redundant database queries for author information. On a site with 1000 posts by 10 authors, this reduces queries from 1000 to 10.
Problem: A new HtmlConverter instance was created for every post, wasting memory and CPU cycles on object initialization.
`php
// Before (inefficient)
$converter = new HtmlConverter( $converter_options );
$converter->getEnvironment()->addConverter( new TableConverter() );
`
Solution: Reuse a single static instance across all post conversions.
`php
// After (optimized)
static $converter = null;
if ( null === $converter ) {
$converter_options = apply_filters( ‘jekyll_export_markdown_converter_options’, array( ‘header_style’ => ‘atx’ ) );
$converter = new HtmlConverter( $converter_options );
$converter->getEnvironment()->addConverter( new TableConverter() );
}
`
Impact: Reduces object creation overhead. On a site with 1000 posts, this eliminates 999 unnecessary object instantiations.
Problem: The copy_recursive() method used the legacy dir() …