Static Site Exporter

Plugin Banner

Static Site Exporter

by Ben Balter

Download
Description

Export your WordPress posts and pages to Markdown in a couple of clicks, ready for Jekyll, Hugo, or any other static site generator.

Features

  • One screen, no settings. Choose Export to Static Site from the Tools menu, click Export, and download a zip. Nothing to configure first.
  • Your content, as Markdown. Posts, pages, and drafts become Markdown files. Each one starts with its title, date, author, categories, tags, and custom fields.
  • Images come along. Your uploaded images and files are included, and links to them keep working.
  • Jekyll, Hugo, Astro, or Eleventy. Jekyll is the default; pick another generator and the export uses its folders and config.
  • Old links keep working. On Jekyll, Hugo, and Eleventy, each post keeps its original web address.

Usage

  1. In your WordPress dashboard, go to Plugins Add New, search for Static Site Exporter, then install and activate it.
  2. Choose Export to Static Site from the Tools menu.
  3. Pick your static site generator (Jekyll is the default), then click Export.
  4. Unzip the download into your site. Output formats says where each generator’s files go.

That’s it. Need more control? You can export from the command line, pick which posts to export, include custom post types, and customize the output.

More information

  1. Choose Export to Static Site from the Tools menu, pick a format, and click Export to download your site as a zip file.

    Choose Export to Static Site from the Tools menu, pick a format, and click Export to download your site as a zip file.

  2. The zip has your posts, drafts, pages, and settings, laid out for Jekyll.

    The zip has your posts, drafts, pages, and settings, laid out for Jekyll.

  3. Each post is Markdown with YAML front matter: title, date, author, categories, tags, and its original URL.

    Each post is Markdown with YAML front matter: title, date, author, categories, tags, and its original URL.

Does it work with Hugo, Eleventy, or other static site generators?

Yes. Pick Hugo, Astro, or Eleventy from the format dropdown under Tools Export to Static Site, or pass --format on the command line, and the export is laid out for that generator, with its folders, draft markers, and config file:

wp jekyll-export --format=hugo --output=export.zip

Jekyll is the default. Every format is Markdown with YAML front matter, which most other generators read too. See Output formats for what each one includes.

My export times out, or the zip file is empty. What can I do?

Large sites can run past PHP’s time or memory limits in the browser. Use WP-CLI instead, which has no time limit, and write the zip straight to a file:

wp jekyll-export --output=export.zip

If your media library is large, add --skip-uploads and copy wp-content/uploads separately. See Performance Tips for Large Sites for more.

Can I export only some of my posts?

Yes. With WP-CLI, filter by category, tag, post type, or status:

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

See Selective Export for every option, including the PHP filters.

Where do my images and other media go?

The export copies wp-content/uploads into the zip and rewrites links to your media as relative URLs (/wp-content/uploads/...), so they keep working on the new site.

Will my old URLs keep working?

Each post’s front matter includes its WordPress URL (permalink, or url for Hugo), so Jekyll, Hugo, and Eleventy publish it at the same address. For Astro, Output formats has a route that does the same.

Are drafts exported?

Yes, into _drafts/ for Jekyll. Hugo, Astro, and Eleventy keep drafts next to your posts, marked draft: true. To leave them out, use wp jekyll-export --post_status=publish.

Does it export custom post types and custom fields?

Custom fields are exported as front matter. Custom post types need one filter to include them, and the export then declares each one as a Jekyll collection. See Custom post types and Custom fields.

Does the export include the text after a “More” tag?

Yes. Each post is exported in full, including everything after a More tag (the “Continue reading” break) and every page of a post split with a Page Break. The More tag stays in the Markdown as <!--more-->, which Hugo treats as the end of a post’s summary. To use it for excerpts in Jekyll, add excerpt_separator: <!--more--> to the front matter of the posts that have one. Setting it in _config.yml instead also works, but then any post without the tag uses its whole text as the excerpt.

Useless

By andybrandt on May 6, 2025

Markdown export is not really Markdown…

Does exactly what it says, nothing more

By andrewjgwise on January 30, 2023

Worked without issue on a WordPress site on shared hosting. Much quicker than using the Jekyll importer.

Try command line version if not working for you

By rpeyron on August 14, 2022

If you have a not so small WordPress site, it is very likely you won’t have any success using this plugin within the WordPress interface, as I can read in the reviews and support messages. But the plugin offer a command line interface that does not have the timeout of the web version. Go inside the plugin folder and run the cli version :
php jekyll-export-cli.php > jekyll-export.zip
The GitHub does provide more information (WordPress forbid to include the link, but the project name seems to be benbalter/wordpress-to-jekyll-exporter) Not doing miracles with plugins, you will have some rework, but definitely working and very useful!

Working in 2022

By turbidplaque on March 17, 2022

I wasn’t expecting much because of the previous reviews indicating this was broken, but having just used it on an (admittedly somewhat small) WP site, I can confirm that it now works. Even with only a couple dozen posts and some pictures, it took a few minutes, so give it time, but the result was exactly as advertised: a .zip file containing all posts as Jekyll-formatted .md files, and the WP uploads directory with all the images. Thanks!

Finally dead

By sinisam on January 8, 2020

It does not work anymore. I suppose it was good while it worked but I will never know.

Broken. doesn't work anymore

By Aslan French (thedonquixotic) on December 19, 2019

Sad because I’d really like to export my posts but doesn’t work.

works still with wp 5.0.2

By andrewufrank on December 25, 2018

I had to export to Hugo and used the plugin with cpanell. upload the zip file to wp-plugins and unpack there. activate and export as instructed. takes long. result has posts in _posts folder and the rest as files; these can easily be placed in correct folders for hugo or jekyll. thank you – saves a lot of time!

Not what I expected.

By vertigoray on May 24, 2018

  1. Non of my blog articles were exported in the zip; only pics and pdfs.
  2. The zip wasn’t valid; had to use 7-zip to force extraction.
Didn’t give 1* cause something was exported. Not at all what I was expecting.

Works as expected

By andeersg on September 3, 2016

Very nice and simple plugin that collects all posts, pages, categories and tags into a zip file.

A big plus for also adding the images and keeping the same folder structure for them.

4.6.0

This release exports for Hugo, Astro, and Eleventy as well as Jekyll, and exports posts in full again. Review the note marked Export change before re-exporting a site you’ve exported before.

  • Export for Hugo, Astro, or Eleventy, not just Jekyll. Pick a format on Tools Export to Static Site, or pass --format to wp jekyll-export. Each format gets that generator’s folders, draft markers, and config file, and the Eleventy export includes a config and stub layouts so it builds as is. Jekyll exports are unchanged. Plugins can add or adjust formats with the jekyll_export_formats filter. See Output formats (#507)
  • The Tools menu item is now Export to Static Site instead of “Export to Jekyll”, and it opens a short screen with the format choice instead of starting the download right away. Hook names, the wp jekyll-export command, and the text domain are unchanged, so scripts and links keep working (#505, #507)
  • The plugin page and README now lead with the one-click export, and WP-CLI, selective export, and large-site tips moved to a new Advanced usage page. The WP-CLI success message can now be translated (#505)
  • Export change: Posts with a More tag (the “Continue reading” break) are exported in full again. Since 2.4.2, the export stopped at the tag and put a “(more…)” link in place of the rest of the post, and posts split with Page Break blocks lost every page after the first. The More tag is kept in the Markdown as <!--more--> on its own line, which Hugo uses to end a post’s summary and Jekyll can use as its excerpt separator (#509, #510). Thanks to @shtrom for the report
  • The readme build now fails if the WordPress.org description goes over 2,500 words, where WordPress.org cuts it off. Repeated and outdated sections had pushed it past that (#511)
  • The plugin now ships its composer.json, so WordPress.org reviewers can see which libraries are bundled in vendor/ (#511)

4.5.0

This release fixes a bug in 4.4.0 that exported every post as HTML instead of Markdown, and updates the HTML-to-Markdown converter for two security fixes, which also change some converted Markdown. Review the notes marked Export change before re-exporting a site you’ve exported before.

  • Export change: Posts are converted to Markdown again. 4.4.0 shipped without several files the bundled converter needs, so every post failed to convert and was exported as its original HTML, without an error. If you exported with 4.4.0, export again
  • Export change: Updated league/html-to-markdown to 5.2.3. It fixes two cases where conversion time grew with the square of a post’s size instead of in proportion to it, so a single large or unusual post could stall an export until PHP timed out (GHSA-h3wg-gqjr-jjmw, GHSA-4xjf-2r66-5r28). It also changes some output: non-breaking spaces are kept instead of becoming regular spaces, headings and paragraphs no longer run onto the same line as an image or other inline content before them, code blocks from <pre> lose a stray leading blank line, links whose text is their URL become autolinks more often, and a line of just - or + is escaped instead of becoming an empty list item. See the upstream changelog for the full list
  • Updated symfony/yaml to 7.4.22, which fixes how some values are quoted and how multi-line text is written, so a few front matter values may be written differently
  • Code samples on the WordPress.org plugin page now render as code. They were shown as plain text, which turned --category and other flags into en dashes, so copied commands failed (#499)
  • A test now pins how the converter escapes Markdown, so a future converter update can’t change exported output unnoticed (#465). Thanks to @theluckystrike

4.4.0

This release updates the HTML-to-Markdown converter for security fixes, which changes some converted Markdown. Review the note marked Export change before re-exporting a site you’ve exported before.

  • Export change: Updated league/html-to-markdown to 5.1.3, a security release. Content inside <pre> and <code>, attribute values of HTML kept as-is, and the href, src, title, and alt of links and images could previously break out of the Markdown they were written into (GHSA-8m5h-gv5q-jmp5, GHSA-vc6h-86×6-wcjg, GHSA-r2cp-59h5-vgjj). The fixes change how backticks, pipes, and link and image attributes are escaped, and keep some code as HTML. See the upstream changelog for the full list
  • The jekyll_export_meta, jekyll_export_terms, jekyll_export_markdown, jekyll_export_html, and jekyll_export_content filters now receive the post being exported as a second argument, so output can be customized per post. Existing one-argument callbacks are unaffected (#463). Thanks to @theluckystrike
  • The jekyll_export_complete action now fires once after a successful export, after the archive is sent or saved. It does not fire if the export fails. The action was documented, but the plugin never fired it (#459). Thanks to @Anandb71
  • Updated symfony/yaml to 7.4.20 (parser fixes only; the exporter only writes YAML)

4.3.0

This release changes what some exports contain. Review the notes marked Export change before re-exporting a site you’ve exported before.

  • Export change: Posts are now selected with WP_Query and an explicit status list (publish, future, draft, pending, private, and inherit), so trashed posts and pages and auto-drafts are no longer written to _drafts/. Custom statuses registered by other plugins (e.g., PublishPress or Edit Flow’s pitch and assigned) are no longer exported either. The list can be changed with the new jekyll_export_post_statuses filter
  • Export change: Only link and media URLs (href, src, and each srcset candidate) are made relative, and only where the site or home URL is followed by a path, query, or fragment. Absolute site URLs in prose, code samples, and query strings are now kept as written, which fixes exports such as the default privacy policy’s “Our website address is: .” Other attributes that pass through as raw HTML, such as the file block’s <object data>, the video block’s poster, and inline background-image styles, now keep their absolute URLs
  • Export change: Posts with []: in a code sample are now converted to Markdown instead of falling back to raw HTML
  • Grandchild pages and nested upload directories are now exported. WP_Filesystem_Direct::mkdir() isn’t recursive, so a page written before its parent (lower post ID) silently failed to write; directories are now created with wp_mkdir_p()
  • Front-matter permalink and image values now strip the site and home URLs in both schemes, so featured images on subdirectory or mixed-scheme installs get relative paths
  • Fatal errors during an export (memory or time limits) now show an export-specific message and guidance on core’s error page. The previous shutdown handler never ran because core’s fatal error handler exits first. The public shutdown_handler() method has been removed
  • --category and --tag for wp jekyll-export now also split on whitespace, as well as commas
  • Uses core helpers (wp_is_writable(), mysql2date(), cache_users()) in place of custom code, and the test suite no longer leaves temporary files behind
  • The WordPress.org listing now has a short description and updated tags, and is tested up to WordPress 7.1

4.2.0

  • send() now discards every output buffer left open by a theme or another plugin before it streams the archive (web requests only; under WP-CLI the caller owns stdout) (#413). A buffer with a callback (an HTML minifier, a CDN rewriter) would otherwise rewrite the binary zip on its way out, and a buffer opened without a chunk size would hold the entire archive in memory until the export died mid-stream. Both reach the browser as a truncated or unreadable download rather than an error page
  • send() now disables zlib.output_compression and opens the archive before tearing down those buffers, so a read failure is still reportable as an admin error page, and checks headers_sent() afterwards, so content that was merely buffered is no longer mistaken for output already on the wire
  • The PHP version check no longer calls wp_die() while the plugin loads. On PHP older than 8.2 that took down the whole site, front end included. The plugin now shows an admin notice and stays inactive. Requires PHP and Requires at least are also declared in the plugin header, so WordPress refuses to activate it on an unsupported version
  • get_posts() no longer caches the list of post IDs in the object cache. The cache key ignored the jekyll_export_post_types and jekyll_export_taxonomy_filters filters and never expired, so on a site with a persistent object cache (Redis, Memcached) a filtered export, such as wp jekyll-export --category=..., could return a previous run’s posts
  • convert_options() no longer runs maybe_unserialize() on values get_option() has already unserialized
  • URL localization now strips the home URL as well as the site URL, so internal links are made relative when WordPress is installed in a subdirectory
  • New --output=<file> option for wp jekyll-export writes the zip to a file instead of STDOUT, so a PHP notice printed during the export can no longer corrupt the archive
  • Posts are now exported in batches of 100. Each batch’s posts, custom fields, terms, and featured images are loaded in a few bulk queries instead of several queries per post, and released from memory afterwards, so large sites export faster and memory use no longer grows with the number of posts
  • Updated symfony/yaml to 7.4.18, which fixes CVE-2026-45304, CVE-2026-45305, and CVE-2026-45133 in its YAML parser. The exporter only writes YAML, so exposure was low. Also updated league/html-to-markdown to 5.1.2
  • The WordPress.org package no longer includes the test suite, CI configuration, docs, or development scripts, so the download is smaller
  • Translations now come exclusively from translate.wordpress.org language packs. The bundled .po files have been removed; none of them contained any translated strings

4.1.1

  • Fixed a zero-byte or unreadable zip download (#413). ZipArchive defers every write to close(), so a full disk, an exhausted quota, or an unwritable temp directory produced a missing archive that the exporter happily streamed as an empty response. zip_folder() now throws when close() fails, and zip() verifies the archive exists and is non-empty before it is sent
  • zip_folder() now detects a failed ZipArchive::open(). open() returns a non-zero integer error code rather than false on failure, so the previous falsy check never fired and every addFile() call silently no-op’d
  • send() opens the archive before emitting any headers and throws if it cannot be read, instead of returning silently after the download headers were already sent
  • send() disables transparent gzip compression (zlib.output_compression) so the Content-Length header cannot disagree with the bytes actually written
  • send() fails with the offending file and line number when a theme or another plugin has already written to the response (a stray blank line or byte order mark), instead of shipping a corrupt archive
  • The temporary zip now uses the same random suffix as the temporary export directory, so concurrent or previously crashed exports cannot collide on a fixed wp-jekyll.zip in a shared temp directory
  • ob_start() now wraps the jekyll_export action, so output echoed by a third-party hook can no longer be prepended to the archive
  • Skip files that vanish mid-export rather than adding a zip entry with an empty name

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