{"id":33288,"date":"2014-12-12T16:23:09","date_gmt":"2014-12-12T16:23:09","guid":{"rendered":"https:\/\/wordpress.org\/plugins-wp\/jekyll-exporter\/"},"modified":"2026-04-30T21:58:33","modified_gmt":"2026-04-30T21:58:33","slug":"jekyll-exporter","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/jekyll-exporter\/","author":148845,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"4.0.4","stable_tag":"4.0.4","tested":"6.9.4","requires":"6.4","requires_php":"8.2","requires_plugins":null,"header_name":"Static Site Exporter","header_author":"Ben Balter","header_description":"","assets_banners_color":"eaeaea","last_updated":"2026-04-30 21:58:33","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/github.com\/benbalter\/wordpress-to-jekyll-exporter\/","header_author_uri":"https:\/\/ben.balter.com","rating":3.5,"author_block_rating":0,"active_installs":500,"downloads":43120,"num_ratings":12,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["changelog","description"],"tags":{"2.0":{"tag":"2.0","author":"benbalter","date":"2014-12-12 16:23:32"},"2.0.1":{"tag":"2.0.1","author":"benbalter","date":"2014-12-15 15:24:00"},"2.1.0":{"tag":"2.1.0","author":"benbalter","date":"2016-01-09 21:04:49"},"2.1.1":{"tag":"2.1.1","author":"benbalter","date":"2016-01-11 17:46:06"},"2.2.0":{"tag":"2.2.0","author":"benbalter","date":"2016-10-30 23:27:23"},"2.2.1":{"tag":"2.2.1","author":"benbalter","date":"2017-08-26 20:10:15"},"2.2.2":{"tag":"2.2.2","author":"benbalter","date":"2017-08-28 20:25:26"},"2.2.3":{"tag":"2.2.3","author":"benbalter","date":"2017-08-29 16:49:17"},"2.3.0":{"tag":"2.3.0","author":"benbalter","date":"2019-02-14 21:05:10"},"2.3.1":{"tag":"2.3.1","author":"benbalter","date":"2021-06-05 22:29:22"},"2.3.2":{"tag":"2.3.2","author":"benbalter","date":"2021-06-10 00:08:03"},"2.3.3":{"tag":"2.3.3","author":"benbalter","date":"2022-01-26 17:49:59"},"2.3.4":{"tag":"2.3.4","author":"benbalter","date":"2022-01-26 19:49:59"},"2.3.5":{"tag":"2.3.5","author":"benbalter","date":"2022-01-26 20:31:25"},"2.3.6":{"tag":"2.3.6","author":"benbalter","date":"2022-01-27 19:30:21"},"2.4.0":{"tag":"2.4.0","author":"benbalter","date":"2024-04-18 18:57:58"},"2.4.1":{"tag":"2.4.1","author":"benbalter","date":"2024-10-16 15:51:53"},"2.4.2":{"tag":"2.4.2","author":"benbalter","date":"2025-05-03 02:08:40"},"3.0.0":{"tag":"3.0.0","author":"benbalter","date":"2025-12-26 19:59:26"},"3.0.1":{"tag":"3.0.1","author":"benbalter","date":"2026-01-12 23:43:17"},"3.0.2":{"tag":"3.0.2","author":"benbalter","date":"2026-01-13 20:49:33"},"3.1.0":{"tag":"3.1.0","author":"benbalter","date":"2026-01-14 18:02:29"},"3.1.1":{"tag":"3.1.1","author":"benbalter","date":"2026-02-09 20:04:44"},"3.1.2":{"tag":"3.1.2","author":"benbalter","date":"2026-04-27 21:07:26"},"3.1.3":{"tag":"3.1.3","author":"benbalter","date":"2026-04-28 18:05:05"},"4.0.0":{"tag":"4.0.0","author":"benbalter","date":"2026-04-28 20:53:30"},"4.0.1":{"tag":"4.0.1","author":"benbalter","date":"2026-04-30 03:16:26"},"4.0.2":{"tag":"4.0.2","author":"benbalter","date":"2026-04-30 21:35:12"},"4.0.3":{"tag":"4.0.3","author":"benbalter","date":"2026-04-30 21:41:36"},"4.0.4":{"tag":"4.0.4","author":"benbalter","date":"2026-04-30 21:58:33"}},"upgrade_notice":[],"ratings":{"1":3,"2":1,"3":1,"4":1,"5":6},"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3073421,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3073421,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3073421,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3073421,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["2.0","2.0.1","2.1.0","2.1.1","2.2.0","2.2.1","2.2.2","2.2.3","2.3.0","2.3.1","2.3.2","2.3.3","2.3.4","2.3.5","2.3.6","2.4.0","2.4.1","2.4.2","3.0.0","3.0.1","3.0.2","3.1.0","3.1.1","3.1.2","3.1.3","4.0.0","4.0.1","4.0.2","4.0.3","4.0.4"],"block_files":[],"assets_screenshots":[],"screenshots":[]},"plugin_section":[],"plugin_tags":[1859,1673,37949,37948,31642],"plugin_category":[59],"plugin_contributors":[78911],"plugin_business_model":[],"class_list":["post-33288","plugin","type-plugin","status-publish","hentry","plugin_tags-export","plugin_tags-github","plugin_tags-github-pages","plugin_tags-jekyll","plugin_tags-yaml","plugin_category-utilities-and-tools","plugin_contributors-benbalter","plugin_committers-benbalter"],"banners":{"banner":"https:\/\/ps.w.org\/jekyll-exporter\/assets\/banner-772x250.png?rev=3073421","banner_2x":"https:\/\/ps.w.org\/jekyll-exporter\/assets\/banner-1544x500.png?rev=3073421","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/jekyll-exporter\/assets\/icon-128x128.png?rev=3073421","icon_2x":"https:\/\/ps.w.org\/jekyll-exporter\/assets\/icon-256x256.png?rev=3073421","generated":false},"screenshots":[],"raw_content":"<!--section=changelog-->\n<h4>4.0.4<\/h4>\n\n<ul>\n<li>Stream the export zip to the browser in 8 KB chunks instead of loading the entire archive into memory in <code>send()<\/code>, so large exports no longer hit <code>memory_limit<\/code> after a successful build<\/li>\n<li><code>zip_folder()<\/code> now throws <code>RuntimeException<\/code> instead of calling <code>wp_die()<\/code> directly, so the existing <code>export()<\/code> try\/catch renders a friendly error and runs <code>cleanup()<\/code> on partial temp files<\/li>\n<li>Added <code>jekyll_export_html_converter<\/code> filter so integrations (and tests) can swap in a custom HTML-to-Markdown converter<\/li>\n<li>Gated the v4.0.3 fallback <code>error_log()<\/code> call behind <code>WP_DEBUG<\/code><\/li>\n<li>Hardened sanitization of <code>$_GET['type']<\/code> in the export callback<\/li>\n<li>Added regression tests for the v4.0.3 <code>Invalid HTML was provided<\/code> fallback and for the new <code>zip_folder()<\/code> throw behavior<\/li>\n<\/ul>\n\n<h4>4.0.3<\/h4>\n\n<ul>\n<li>Catch <code>InvalidArgumentException<\/code> from <code>league\/html-to-markdown<\/code> in <code>convert_content()<\/code> 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\" (<a href=\"https:\/\/github.com\/benbalter\/wordpress-static-site-exporter\/issues\/400\">#400<\/a>)<\/li>\n<\/ul>\n\n<h4>4.0.2<\/h4>\n\n<ul>\n<li>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<\/li>\n<li>Add proactive <code>memory_limit<\/code> pre-flight check (warns when below 64MB) in <code>validate_environment()<\/code><\/li>\n<li>Display admin error notice on Tools \u2192 Export when environment validation fails, before the user clicks Export<\/li>\n<\/ul>\n\n<h4>4.0.1<\/h4>\n\n<ul>\n<li>Security: Use cryptographically secure randomness (<code>wp_generate_password<\/code>) instead of <code>md5(time())<\/code> for the export temp directory name to prevent symlink\/TOCTOU attacks on shared hosts (CWE-330\/377)<\/li>\n<li>Security: Reject non-CLI access in deprecated <code>jekyll-export-cli.php<\/code> before bootstrapping WordPress (CWE-665)<\/li>\n<li>Security: Sanitize each path segment of page filenames as defense-in-depth against path traversal (CWE-22)<\/li>\n<li>Fix stale <code>$upload_basedir<\/code> cache in <code>copy_recursive()<\/code> on multisite by keying it on the current blog ID<\/li>\n<\/ul>\n\n<h4>4.0.0<\/h4>\n\n<ul>\n<li><strong>Breaking:<\/strong> Minimum PHP version bumped from 7.2.5 to 8.2<\/li>\n<li><strong>Breaking:<\/strong> Minimum WordPress version bumped from 4.4 to 6.4<\/li>\n<li>Updated <code>symfony\/yaml<\/code> from ^5.4 to ^7.0<\/li>\n<li>Updated PHPUnit from ~8.0 to ~9.6<\/li>\n<li>Removed <code>symfony\/polyfill-php80<\/code> (no longer needed)<\/li>\n<li>Added PHPStan static analysis at level 5<\/li>\n<li>Fixed <code>get_posts()<\/code> to return integer IDs instead of strings<\/li>\n<li>Fixed PHPDoc type annotations throughout codebase<\/li>\n<li>Deprecated legacy <code>jekyll-export-cli.php<\/code> in favor of <code>lib\/cli.php<\/code><\/li>\n<li>Improved CI pipeline with PHPStan job and vendor consistency checks<\/li>\n<\/ul>\n\n<p><a href=\"https:\/\/github.com\/benbalter\/wordpress-to-jekyll-exporter\/releases\">View Past Releases<\/a><\/p>\n\n<!--section=description-->\n<h3>Features<\/h3>\n\n<ul>\n<li>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)<\/li>\n<li>Export what your users see, not what the database stores (runs post content through <code>the_content<\/code> filter prior to export, allowing third-party plugins to modify the output)<\/li>\n<li>Converts all <code>post_content<\/code> to Markdown<\/li>\n<li>Converts all <code>post_meta<\/code> and fields within the <code>wp_posts<\/code> table to YAML front matter for parsing by Jekyll<\/li>\n<li>Generates a <code>_config.yml<\/code> with all settings in the <code>wp_options<\/code> table<\/li>\n<li>Outputs a single zip file with <code>_config.yml<\/code>, pages, and <code>_posts<\/code> folder containing <code>.md<\/code> files for each post in the proper Jekyll naming convention<\/li>\n<li><strong>Selective export<\/strong>: Export only specific categories, tags, or post types using WP-CLI<\/li>\n<li>No settings. Just a single click.<\/li>\n<\/ul>\n\n<h3>Usage<\/h3>\n\n<ol>\n<li>Place plugin in <code>\/wp-content\/plugins\/<\/code> folder<\/li>\n<li>Activate plugin in WordPress dashboard<\/li>\n<li>Select <code>Export to Jekyll<\/code> from the <code>Tools<\/code> menu<\/li>\n<\/ol>\n\n<h3>More information<\/h3>\n\n<p>See <a href=\"https:\/\/ben.balter.com\/wordpress-to-jekyll-exporter\">the full documentation<\/a>:<\/p>\n\n<ul>\n<li><a href=\"https:\/\/ben.balter.com\/wordpress-to-jekyll-exporter\/.\/docs\/changelog\/\">Changelog<\/a><\/li>\n<li><a href=\"https:\/\/ben.balter.com\/wordpress-to-jekyll-exporter\/.\/docs\/command-line-usage\/\">Command-line-usage<\/a><\/li>\n<li><a href=\"https:\/\/ben.balter.com\/wordpress-to-jekyll-exporter\/.\/docs\/selective-export\/\">Selective export by category or tag<\/a><\/li>\n<li><a href=\"https:\/\/ben.balter.com\/wordpress-to-jekyll-exporter\/.\/docs\/custom-post-types\/\">Custom post types<\/a><\/li>\n<li><a href=\"https:\/\/ben.balter.com\/wordpress-to-jekyll-exporter\/.\/docs\/custom-fields\/\">Custom fields<\/a><\/li>\n<li><a href=\"https:\/\/ben.balter.com\/wordpress-to-jekyll-exporter\/.\/docs\/developing-locally\/\">Developing locally<\/a><\/li>\n<li><a href=\"https:\/\/ben.balter.com\/wordpress-to-jekyll-exporter\/.\/docs\/required-php-version\/\">Minimum required PHP version<\/a><\/li>\n<\/ul>\n\n<h3>Selective Export by Category or Tag<\/h3>\n\n<p>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:<\/p>\n\n<ul>\n<li>You have a large WordPress site but only need to convert specific sections<\/li>\n<li>You want to migrate content by topic or category<\/li>\n<li>You need to export content incrementally<\/li>\n<\/ul>\n\n<h3>Using WP-CLI<\/h3>\n\n<p>The easiest way to perform selective exports is via WP-CLI commands.<\/p>\n\n<h4>Export by Category<\/h4>\n\n<p>To export posts from a single category, use the category slug:<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --category=technology &gt; technology-export.zip\n    `<\/p>\n\n<p>To export from multiple categories (OR logic - posts in any of these categories):<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --category=tech,news,updates &gt; export.zip\n    `<\/p>\n\n<h4>Export by Tag<\/h4>\n\n<p>To export posts with a specific tag:<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --tag=featured &gt; featured-export.zip\n    `<\/p>\n\n<p>To export posts with multiple tags (OR logic):<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --tag=featured,popular &gt; export.zip\n    `<\/p>\n\n<h4>Export Specific Post Types<\/h4>\n\n<p>To export only pages:<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --post_type=page &gt; pages-export.zip\n    `<\/p>\n\n<p>To export only posts:<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --post_type=post &gt; posts-export.zip\n    `<\/p>\n\n<p>To export custom post types:<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --post_type=portfolio,testimonial &gt; custom-export.zip\n    `<\/p>\n\n<h4>Combining Filters<\/h4>\n\n<p>You can combine multiple filters. Posts must match ALL specified filters (AND logic):<\/p>\n\n<pre><code>`bash&lt;h3&gt;Export posts that are in \"technology\" category AND have \"featured\" tag&lt;\/h3&gt;wp jekyll-export --category=technology --tag=featured --post_type=post &gt; export.zip\n`&lt;h3&gt;Using PHP Filters&lt;\/h3&gt;\n<\/code><\/pre>\n\n<p>For more programmatic control, you can use WordPress filters directly in your theme's <code>functions.php<\/code> or a custom plugin.<\/p>\n\n<h4>Filter by Category<\/h4>\n\n<pre><code>`php\n<\/code><\/pre>\n\n<p>add_filter( 'jekyll_export_taxonomy_filters', function() {\n    return array(\n        'category' =&gt; array( 'technology', 'science' ),\n    );\n} );\n    `<\/p>\n\n<h4>Filter by Tag<\/h4>\n\n<pre><code>`php\n<\/code><\/pre>\n\n<p>add_filter( 'jekyll_export_taxonomy_filters', function() {\n    return array(\n        'post_tag' =&gt; array( 'featured', 'popular' ),\n    );\n} );\n    `<\/p>\n\n<h4>Filter by Custom Taxonomy<\/h4>\n\n<pre><code>`php\n<\/code><\/pre>\n\n<p>add_filter( 'jekyll_export_taxonomy_filters', function() {\n    return array(\n        'my_custom_taxonomy' =&gt; array( 'term-slug-1', 'term-slug-2' ),\n    );\n} );\n    `<\/p>\n\n<h4>Combine Multiple Taxonomies<\/h4>\n\n<pre><code>`php\n<\/code><\/pre>\n\n<p>add_filter( 'jekyll_export_taxonomy_filters', function() {\n    return array(\n        'category' =&gt; array( 'technology' ),\n        'post_tag' =&gt; array( 'featured' ),\n        'custom_tax' =&gt; array( 'term-1' ),\n    );\n} );\n    `<\/p>\n\n<h4>Filter Post Types<\/h4>\n\n<pre><code>`php\n<\/code><\/pre>\n\n<p>add_filter( 'jekyll_export_post_types', function() {\n    return array( 'post', 'page' ); \/\/ Only export posts and pages\n} );\n    `<\/p>\n\n<h3>Finding Category and Tag Slugs<\/h3>\n\n<p>If you're not sure what slug to use:<\/p>\n\n<h4>Via WordPress Admin<\/h4>\n\n<ol>\n<li>Go to <strong>Posts &gt; Categories<\/strong> or <strong>Posts &gt; Tags<\/strong><\/li>\n<li>Hover over the category\/tag name<\/li>\n<li>Look at the browser's status bar or the URL - you'll see something like <code>tag_ID=123&amp;taxonomy=post_tag&amp;term_slug=featured<\/code><\/li>\n<li>The slug is the part after <code>term_slug=<\/code><\/li>\n<\/ol>\n\n<h4>Via WP-CLI<\/h4>\n\n<p>List all categories with their slugs:<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp term list category --fields=name,slug\n    `<\/p>\n\n<p>List all tags with their slugs:<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp term list post_tag --fields=name,slug\n    `<\/p>\n\n<h3>Use Cases<\/h3>\n\n<h4>Scenario 1: Export a Single Blog Section<\/h4>\n\n<p>You have a WordPress site with multiple sections (Tech, Lifestyle, Travel) and want to move just the Tech section to a static site:<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --category=tech &gt; tech-blog-export.zip\n    `<\/p>\n\n<h4>Scenario 2: Export Featured Content<\/h4>\n\n<p>You want to export only posts marked as \"featured\" for a special showcase site:<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --tag=featured &gt; featured-content.zip\n    `<\/p>\n\n<h4>Scenario 3: Export by Year (using custom taxonomy)<\/h4>\n\n<p>If you've tagged posts by year, you can export by year:<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --tag=2024 &gt; 2024-posts.zip\n    `<\/p>\n\n<h4>Scenario 4: Migrate Content Incrementally<\/h4>\n\n<p>Export different categories separately for incremental migration:<\/p>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --category=tech &gt; tech.zip\nwp jekyll-export --category=news &gt; news.zip\nwp jekyll-export --category=reviews &gt; reviews.zip\n    `<\/p>\n\n<h3>Technical Details<\/h3>\n\n<ul>\n<li><strong>Taxonomy Filtering<\/strong>: Uses WordPress term slugs (not names or IDs)<\/li>\n<li><strong>Query Performance<\/strong>: Filtering is done at the database level for efficiency<\/li>\n<li><strong>OR Logic Within Taxonomy<\/strong>: Multiple terms in the same taxonomy use OR logic (e.g., posts in category A OR B)<\/li>\n<li><strong>AND Logic Across Taxonomies<\/strong>: Multiple taxonomies use AND logic (e.g., posts in category A AND having tag B)<\/li>\n<li><strong>Post Type Filtering<\/strong>: Works independently of taxonomy filtering<\/li>\n<\/ul>\n\n<h3>Limitations<\/h3>\n\n<ul>\n<li>Revisions are excluded when using taxonomy filters (as they don't have taxonomy terms)<\/li>\n<li>Taxonomy filtering uses term slugs, not term IDs or names<\/li>\n<li>Empty taxonomy filters are ignored (no filtering applied)<\/li>\n<\/ul>\n\n<h3>Troubleshooting<\/h3>\n\n<h4>No Posts Exported<\/h4>\n\n<p>If your export is empty:<\/p>\n\n<ol>\n<li><strong>Check the slug<\/strong>: Make sure you're using the term slug, not the name\n\n<ul>\n<li>Use <code>wp term list category<\/code> to verify the exact slug<\/li>\n<\/ul><\/li>\n<li><strong>Check post status<\/strong>: Only published, future, and draft posts are exported<\/li>\n<li><strong>Verify taxonomy<\/strong>: Make sure you're using the correct taxonomy name (<code>category<\/code>, <code>post_tag<\/code>, etc.)<\/li>\n<\/ol>\n\n<h4>Wrong Posts Exported<\/h4>\n\n<p>If you're getting unexpected posts:<\/p>\n\n<ol>\n<li><strong>Check term associations<\/strong>: Verify which posts have the category\/tag assigned<\/li>\n<li><strong>Review filter logic<\/strong>: Remember that multiple categories use OR logic<\/li>\n<li><strong>Clear cache<\/strong>: If testing, use <code>wp cache flush<\/code> between exports<\/li>\n<\/ol>\n\n<h3>Custom post types<\/h3>\n\n<p>To export custom post types, you'll need to add a filter (w.g. to your themes config file) to do the following:<\/p>\n\n<pre><code>`php\n<\/code><\/pre>\n\n<p>add_filter( 'jekyll_export_post_types', function() {\n    return array('post', 'page', 'you-custom-post-type');\n});\n    `<\/p>\n\n<p>The custom post type will be exported as a Jekyll collection. You'll need to initialize it in the resulting Jekyll site's <code>_config.yml<\/code>.<\/p>\n\n<h3>Developing locally<\/h3>\n\n<h4>Option 1: Using Dev Containers (Recommended)<\/h4>\n\n<p>The easiest way to get started is using <a href=\"https:\/\/code.visualstudio.com\/docs\/devcontainers\/containers\">VS Code Dev Containers<\/a> or <a href=\"https:\/\/github.com\/features\/codespaces\">GitHub Codespaces<\/a>:<\/p>\n\n<ol>\n<li>Install <a href=\"https:\/\/code.visualstudio.com\/\">VS Code<\/a> and the <a href=\"https:\/\/marketplace.visualstudio.com\/items?itemName=ms-vscode-remote.remote-containers\">Dev Containers extension<\/a><\/li>\n<li><code>git clone https:\/\/github.com\/benbalter\/wordpress-to-jekyll-exporter<\/code><\/li>\n<li>Open the folder in VS Code<\/li>\n<li>Click \"Reopen in Container\" when prompted<\/li>\n<li>Wait for the container to build and dependencies to install<\/li>\n<li>Access WordPress at <code>http:\/\/localhost:8088<\/code><\/li>\n<\/ol>\n\n<p>The devcontainer includes:\n- Pre-configured WordPress and MySQL\n- All PHP extensions and Composer dependencies\n- VS Code extensions for PHP development, debugging, and testing\n- WordPress coding standards configured<\/p>\n\n<p>See <a href=\"https:\/\/ben.balter.com\/wordpress-to-jekyll-exporter\/.\/.devcontainer\/README\/\">.devcontainer\/README.md<\/a> for more details.<\/p>\n\n<h4>Option 2: Manual Setup<\/h4>\n\n<h4>Prerequisites<\/h4>\n\n<ol>\n<li><code>sudo apt-get update<\/code><\/li>\n<li><code>sudo apt-get install composer<\/code><\/li>\n<li><code>sudo apt-get install php7.3-xml<\/code><\/li>\n<li><code>sudo apt-get install php7.3-mysql<\/code><\/li>\n<li><code>sudo apt-get install php7.3-zip<\/code><\/li>\n<li><code>sudo apt-get install php-mbstring<\/code><\/li>\n<li><code>sudo apt-get install subversion<\/code><\/li>\n<li><code>sudo apt-get install mysql-server<\/code><\/li>\n<li><code>sudo apt-get install php-pear<\/code><\/li>\n<li><code>sudo pear install PHP_CodeSniffer<\/code><\/li>\n<\/ol>\n\n<h4>Bootstrap &amp; Setup<\/h4>\n\n<ol>\n<li><code>git clone https:\/\/github.com\/benbalter\/wordpress-to-jekyll-exporter<\/code><\/li>\n<li><code>cd wordpress-to-jekyll-exporter<\/code><\/li>\n<li><code>script\/bootstrap<\/code><\/li>\n<li><code>script\/setup<\/code><\/li>\n<\/ol>\n\n<h4>Option 3: Docker Compose Only<\/h4>\n\n<ol>\n<li><code>git clone https:\/\/github.com\/benbalter\/wordpress-to-jekyll-exporter<\/code><\/li>\n<li><code>docker-compose up<\/code><\/li>\n<li><code>open localhost:8088<\/code><\/li>\n<\/ol>\n\n<h3>Running tests<\/h3>\n\n<pre><code>script\/cibuild&lt;h3&gt;Custom fields&lt;\/h3&gt;\n<\/code><\/pre>\n\n<p>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.<\/p>\n\n<h4>Available Filters<\/h4>\n\n<p>The plugin provides two filters for customizing post metadata:<\/p>\n\n<ul>\n<li><strong><code>jekyll_export_meta<\/code><\/strong>: Filters the metadata for a single post before it's merged with taxonomy terms. Receives <code>$meta<\/code> array as the only parameter.<\/li>\n<li><strong><code>jekyll_export_post_meta<\/code><\/strong>: Filters the complete metadata array (including taxonomy terms) just before it's written to the YAML frontmatter. Receives <code>$meta<\/code> array and <code>$post<\/code> object as parameters. This is the recommended filter for most use cases.<\/li>\n<\/ul>\n\n<p><strong>Note:<\/strong> 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 <code>jekyll_export_post_meta<\/code> filter to customize this behavior.<\/p>\n\n<p>By default, the plugin saves custom fields in an array structure that is exported as:<\/p>\n\n<pre><code>`php\n<\/code><\/pre>\n\n<p>[\"my-bool\"]=&gt;\n    array(1) {\n        [0] =&gt; string(1) \"1\"\n    }\n[\"location\"]=&gt;\n    array(1) {\n        [0] =&gt; string(88) \"My address\"\n    }\n    `<\/p>\n\n<p>And this leads to a YAML structure like:<\/p>\n\n<pre><code>`yaml\n<\/code><\/pre>\n\n<p>my-bool:\n- \"1\"\nlocation:\n- 'My address'\n    `<\/p>\n\n<p>This is likely not the structure you expect or want to work with. You can convert it using a filter:<\/p>\n\n<pre><code>`php\n<\/code><\/pre>\n\n<p>add_filter( 'jekyll_export_meta', function($meta) {\n    foreach ($meta as $key =&gt; $value) {\n        if (is_array($value) &amp;&amp; count($value) === 1 &amp;&amp; array_key_exists(0, $value)) {\n            $meta[$key] = $value[0];\n        }\n    }<\/p>\n\n<pre><code>return $meta;\n<\/code><\/pre>\n\n<p>});\n    `<\/p>\n\n<p>A more complete solution could look like that:<\/p>\n\n<pre><code>`php\n<\/code><\/pre>\n\n<p>add_filter( 'jekyll_export_meta', function($meta) {\n    foreach ($meta as $key =&gt; $value) {\n        \/\/ Advanced Custom Fields\n        if (is_array($value) &amp;&amp; count($value) === 1 &amp;&amp; array_key_exists(0, $value)) {\n            $value = maybe_unserialize($value[0]);\n            \/\/ Advanced Custom Fields: NextGEN Gallery Field add-on\n            if (is_array($value) &amp;&amp; count($value) === 1 &amp;&amp; array_key_exists(0, $value)) {\n                $value = $value[0];\n            }\n        }\n        \/\/ convert types\n        $value = match ($key) {\n            \/\/ Advanced Custom Fields: \"true_false\" type\n            'my-bool' =&gt; (bool) $value,\n            default =&gt; $value\n        };\n        $meta[$key] = $value;\n    }<\/p>\n\n<pre><code>return $meta;\n<\/code><\/pre>\n\n<p>});\n    `<\/p>\n\n<h4>Removing Empty or Falsy Values<\/h4>\n\n<p>If you want to remove empty or falsy values from the frontmatter (similar to the pre-3.0.3 behavior), you can use the <code>jekyll_export_post_meta<\/code> filter:<\/p>\n\n<pre><code>`php\n<\/code><\/pre>\n\n<p>add_filter( 'jekyll_export_post_meta', function( $meta, $post ) {\n    foreach ( $meta as $key =&gt; $value ) {\n        \/\/ Remove falsy values except numeric 0\n        if ( ! is_numeric( $value ) &amp;&amp; ! $value ) {\n            unset( $meta[ $key ] );\n        }\n    }\n    return $meta;\n}, 10, 2 );\n    `<\/p>\n\n<h3>Command-line Usage<\/h3>\n\n<p>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.<\/p>\n\n<p>It works just like the plugin, but produces the zipfile on STDOUT:<\/p>\n\n<pre><code>`\n<\/code><\/pre>\n\n<p>php jekyll-export-cli.php &gt; jekyll-export.zip\n    `<\/p>\n\n<p>If using this method, you must run first <code>cd<\/code> into the wordpress-to-jekyll-exporter directory.<\/p>\n\n<p>Alternatively, if you have <a href=\"http:\/\/wp-cli.org\">WP-CLI<\/a> installed, you can run:<\/p>\n\n<pre><code>`\n<\/code><\/pre>\n\n<p>wp jekyll-export &gt; export.zip\n    `<\/p>\n\n<p>The WP-CLI version will provide greater compatibility for alternate WordPress environments, such as when <code>wp-content<\/code> isn't in the usual location.<\/p>\n\n<h3>Filtering by Category or Tag<\/h3>\n\n<p>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.<\/p>\n\n<h4>Export posts from a specific category:<\/h4>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --category=technology &gt; export.zip\n    `<\/p>\n\n<h4>Export posts from multiple categories:<\/h4>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --category=tech,news,updates &gt; export.zip\n    `<\/p>\n\n<h4>Export posts with a specific tag:<\/h4>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --tag=featured &gt; export.zip\n    `<\/p>\n\n<h4>Export only pages (or specific post types):<\/h4>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --post_type=page &gt; export.zip\n    `<\/p>\n\n<h4>Combine filters:<\/h4>\n\n<pre><code>`bash\n<\/code><\/pre>\n\n<p>wp jekyll-export --category=technology --tag=featured --post_type=post &gt; export.zip\n    <code>&lt;h3&gt;Using Filters in PHP&lt;\/h3&gt;\nIf you're using the plugin via PHP code or want more control, you can use the<\/code>jekyll_export_taxonomy_filters` filter:<\/p>\n\n<pre><code>`php\n<\/code><\/pre>\n\n<p>add_filter( 'jekyll_export_taxonomy_filters', function() {\n    return array(\n        'category' =&gt; array( 'technology', 'science' ),\n        'post_tag' =&gt; array( 'featured' ),\n    );\n} );<\/p>\n\n<p>\/\/ Then trigger the export\nglobal $jekyll_export;\n$jekyll_export-&gt;export();\n    `<\/p>\n\n<h3>Test Coverage Improvements<\/h3>\n\n<h3>Overview<\/h3>\n\n<p>This document summarizes the comprehensive testing improvements made to the WordPress to Jekyll Exporter plugin.<\/p>\n\n<h3>Test Files Added<\/h3>\n\n<h4>1. `tests\/test-cli.php` - CLI Command Tests<\/h4>\n\n<p>Tests for the WP-CLI integration functionality:\n- Verifies <code>Jekyll_Export_Command<\/code> class exists when WP_CLI is defined\n- Tests that the command has the required <code>__invoke<\/code> method\n- Validates command instantiation<\/p>\n\n<h4>2. `tests\/test-integration.php` - Integration Tests<\/h4>\n\n<p>Comprehensive integration tests for the full export workflow:\n- Full export workflow validation (config + posts + uploads)\n- Zip file creation and contents verification\n- Multi-post type handling (posts, pages, drafts)\n- Upload file copying and export\n- Special character handling in titles\n- End-to-end YAML front matter validation\n- Markdown conversion validation<\/p>\n\n<h4>3. `tests\/test-edge-cases.php` - Edge Case Tests<\/h4>\n\n<p>Tests for edge cases and error conditions:\n- Posts with very long titles\n- Unicode characters (\u00e9mojis, \u4e2d\u6587, \u0627\u0644\u0639\u0631\u0628\u064a\u0629)\n- HTML in post titles\n- Table conversion to Markdown\n- Shortcode processing\n- Serialized post meta data\n- Empty post slugs\n- Post formats\n- Serialized options\n- Symbolic links\n- Empty post lists\n- Invalid dates<\/p>\n\n<h3>Enhanced Tests in `test-wordpress-to-jekyll-exporter.php`<\/h3>\n\n<p>Added comprehensive tests for previously untested or under-tested functions:<\/p>\n\n<h4>New Function Tests<\/h4>\n\n<ol>\n<li><strong><code>test_filesystem_method_filter()<\/code><\/strong> - Verifies the filesystem method filter returns 'direct'<\/li>\n<li><strong><code>test_register_menu()<\/code><\/strong> - Tests menu registration in WordPress admin<\/li>\n<li><strong><code>test_zip_folder_empty()<\/code><\/strong> - Tests zip creation with empty directories<\/li>\n<li><strong><code>test_zip_folder_nested()<\/code><\/strong> - Tests zip creation with nested directory structures<\/li>\n<\/ol>\n\n<h4>New Edge Case Tests<\/h4>\n\n<ol>\n<li><strong><code>test_convert_meta_no_custom_fields()<\/code><\/strong> - Tests meta conversion without custom fields<\/li>\n<li><strong><code>test_convert_meta_with_featured_image()<\/code><\/strong> - Tests featured image handling in meta<\/li>\n<li><strong><code>test_convert_terms_no_terms()<\/code><\/strong> - Tests term conversion when no terms exist<\/li>\n<li><strong><code>test_convert_content_empty()<\/code><\/strong> - Tests conversion of empty content<\/li>\n<li><strong><code>test_convert_content_complex_html()<\/code><\/strong> - Tests conversion of complex HTML (headings, links, lists)<\/li>\n<li><strong><code>test_write_draft()<\/code><\/strong> - Tests writing draft posts to <code>_drafts<\/code> directory<\/li>\n<li><strong><code>test_write_future()<\/code><\/strong> - Tests writing future posts to <code>_posts<\/code> directory<\/li>\n<li><strong><code>test_write_subpage()<\/code><\/strong> - Tests writing sub-pages with correct paths<\/li>\n<li><strong><code>test_rename_key_nonexistent()<\/code><\/strong> - Tests rename_key with non-existent keys<\/li>\n<li><strong><code>test_convert_options_filters_hidden()<\/code><\/strong> - Tests that hidden options are filtered<\/li>\n<li><strong><code>test_get_posts_caching()<\/code><\/strong> - Tests post caching mechanism<\/li>\n<li><strong><code>test_copy_recursive_skips_temp()<\/code><\/strong> - Tests that temporary directories are skipped<\/li>\n<\/ol>\n\n<h3>Test Coverage Summary<\/h3>\n\n<h4>Previously Tested Functions<\/h4>\n\n<ul>\n<li>\u2705 Plugin activation<\/li>\n<li>\u2705 Dependency loading<\/li>\n<li>\u2705 Getting post IDs<\/li>\n<li>\u2705 Converting meta (basic)<\/li>\n<li>\u2705 Converting terms (basic)<\/li>\n<li>\u2705 Converting content (basic)<\/li>\n<li>\u2705 Temp directory initialization<\/li>\n<li>\u2705 Converting posts<\/li>\n<li>\u2705 Exporting options<\/li>\n<li>\u2705 Writing files<\/li>\n<li>\u2705 Creating zip<\/li>\n<li>\u2705 Cleanup<\/li>\n<li>\u2705 Rename key<\/li>\n<li>\u2705 Converting uploads<\/li>\n<li>\u2705 Copy recursive (basic)<\/li>\n<\/ul>\n\n<h4>Newly Added Test Coverage<\/h4>\n\n<ul>\n<li>\u2705 CLI command functionality<\/li>\n<li>\u2705 Filesystem method filter<\/li>\n<li>\u2705 Menu registration<\/li>\n<li>\u2705 Featured images in meta<\/li>\n<li>\u2705 Complex HTML to Markdown conversion<\/li>\n<li>\u2705 Draft and future post handling<\/li>\n<li>\u2705 Sub-page path handling<\/li>\n<li>\u2705 Empty and edge case content<\/li>\n<li>\u2705 Hidden option filtering<\/li>\n<li>\u2705 Post caching<\/li>\n<li>\u2705 Temporary directory exclusion<\/li>\n<li>\u2705 Full export workflow integration<\/li>\n<li>\u2705 Zip contents validation<\/li>\n<li>\u2705 Multi-post type exports<\/li>\n<li>\u2705 Unicode character handling<\/li>\n<li>\u2705 HTML in titles<\/li>\n<li>\u2705 Table conversion<\/li>\n<li>\u2705 Shortcode processing<\/li>\n<li>\u2705 Serialized data handling<\/li>\n<li>\u2705 Symbolic link handling<\/li>\n<li>\u2705 Long titles<\/li>\n<li>\u2705 Post formats<\/li>\n<li>\u2705 Special characters<\/li>\n<\/ul>\n\n<h3>Coverage Statistics<\/h3>\n\n<h4>Original Test File<\/h4>\n\n<ul>\n<li><strong>Lines<\/strong>: 415<\/li>\n<li><strong>Test Functions<\/strong>: 15<\/li>\n<\/ul>\n\n<h4>Enhanced Test Files<\/h4>\n\n<ul>\n<li><strong>test-wordpress-to-jekyll-exporter.php<\/strong>: 699 lines (+284), 31 test functions (+16)<\/li>\n<li><strong>test-cli.php<\/strong>: 60 lines (new), 3 test functions (new)<\/li>\n<li><strong>test-integration.php<\/strong>: 247 lines (new), 6 test functions (new)<\/li>\n<li><strong>test-edge-cases.php<\/strong>: 273 lines (new), 15 test functions (new)<\/li>\n<\/ul>\n\n<p>= Total  &hellip;<\/p>","raw_excerpt":"Features","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/33288","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=33288"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/benbalter"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=33288"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=33288"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=33288"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=33288"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=33288"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=33288"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}