Export WP Pages
to Static HTML
Surgical, flexible static HTML export for WordPress. Export individual posts, hand-picked pages, or any custom post type — in any status, rendered as any user role.
Overview
§ 01Export WP Pages to Static HTML is a WordPress plugin that converts your dynamic WordPress content into clean, self-contained static HTML files. Unlike full-site generators that convert everything in one go, this plugin gives you surgical control — exporting exactly the content you need, in the status you want, as the user role you choose.
The plugin is built around a fault-tolerant background export engine that uses WordPress cron, a database queue, exponential backoff for retries, a watchdog for stalled processes, and adaptive batch sizing — making it suitable for everything from a single landing page to large multi-post exports.
Why Export WP Pages to Static HTML?
/images, /css, and /js directories for clean, developer-ready output. Flatten parent post URLs to root-level .html files.What It Is Not Suitable For
Requirements
§ 02| Component | Minimum | Recommended | Notes |
|---|---|---|---|
| WordPress | 5.8 | 6.7+ | Tested up to 6.7 |
| PHP | 7.4 | 8.1+ | PHP 8.x gives better performance |
| PHP ZipArchive | Required for ZIP | — | For auto-ZIP creation; gracefully skipped if absent |
| PHP FTP extension | Optional | — | Required only if using FTP/SFTP delivery |
| WP-Cron / Server Cron | Required | Server cron | Background processing relies on WP-Cron or a server-side cron job |
| Writable uploads | Required | — | Exports written to wp-content/wp-to-html-exports/ |
| REST API | Required | — | Plugin uses its own wp_to_html/v1/ namespace |
| MySQL / MariaDB | Any WP-supported | — | Creates 3 custom tables on activation |
Theme & Plugin Compatibility
Compatible with all WordPress themes including block themes and classic themes. Tested page builders include Elementor, Divi, Beaver Builder, Bricks Builder, and Gutenberg. SEO plugins (Yoast SEO, Rank Math, AIOSEO, SEOPress) work correctly. All public registered custom post types are auto-detected.
Installation
§ 03Automatic (Recommended)
Export WP Pages to Static HTML, then click Install Now and Activate.Manual Installation
.zip file from WordPress.org.wp_to_html_queue, wp_to_html_assets, wp_to_html_status) and the export directory at wp-content/wp-to-html-exports/.wp-to-html-exports/ are preserved.Quick Start
§ 04Export Scopes
§ 05| Scope | Tier | Description |
|---|---|---|
| Custom (Selected) | Free | Hand-pick individual posts, pages, or custom post type items. Use the search field or browse tabs by content type. Supports multi-select and "Select All" for bulk picks within the current view. |
| All Posts | Pro | Export every post (or selected custom post types) across all configured statuses in one run. |
| All Pages | Pro | Export your entire pages library in one click. |
| Full Site | Pro | Complete WordPress-to-static-HTML conversion including all public post types. |
wp_to_html_allowed_scopes() and can be extended via the wp_to_html/allowed_scopes filter. The Pro plugin can define the WP_TO_HTML_PRO_ACTIVE constant or load the WpToHtml_Pro\Plugin class to unlock Pro scopes.Post Status Support
§ 06One of the key differentiators of this plugin is support for all five WordPress post statuses. Most static export plugins only handle published content. This plugin lets you export content in any state, making it ideal for client previews and archiving.
| Status | Slug | Use Case |
|---|---|---|
| Published | publish | Standard live content export |
| Draft | draft | Preview unpublished pages before they go live |
| Private | private | Export content visible only to logged-in users |
| Pending Review | pending | Export content awaiting editorial approval |
| Scheduled | future | Export pre-scheduled posts before their publish date |
Role-Based Export
§ 07Role-based export allows you to render pages exactly as a specific WordPress user role would see them. This is essential for membership sites, gated content previews, and delivering client work that correctly reflects role-dependent visibility.
How it Works
wp_create_user() with a randomised login and password, assigns the chosen role, and flags the user with wp_to_html_temp_export_user = 1 meta. The password is never stored.wp_generate_auth_cookie() and injected into each HTTP request made to fetch pages. Cookies are never persisted to the database.wp_delete_user(). Only users flagged with the wp_to_html_temp_export_user meta are ever deleted — your real users are never touched.Asset Options
§ 08Asset Collection Modes
| Mode | What Gets Included | Best For |
|---|---|---|
| Strict | Only assets directly referenced (CSS, JS, images) by the exported pages themselves | Minimal, lean exports; known external CDN assets |
| Hybrid Default | Directly referenced assets plus the full WordPress media library | Most use cases; balances completeness and performance |
| Full | Referenced assets + media library + theme assets + all plugin assets | Full site archiving; maximum fidelity |
Group Assets by Type
When Group assets by type is enabled, all exported assets are automatically sorted into clean subdirectories:
/wp-to-html-exports/ ├── index.html ├── about.html ├── images/ │ ├── logo.png │ └── hero.jpg ├── css/ │ ├── style.css │ └── theme.min.css └── js/ ├── app.js └── vendor.min.js
Parent Posts in Root Directory
When Parent posts in root dir is enabled, parent-level posts are flattened to root-level .html files instead of nested directories:
| Option | Output Path |
|---|---|
| Disabled (default) | /about/index.html |
| Enabled | /about.html |
URL Discovery & Crawling
§ 09Before any HTML is exported, the plugin uses an intelligent URL discovery system to collect all URLs that need to be fetched. The Url_Discovery class orchestrates a set of specialised crawler classes, each targeting a different class of WordPress URL.
| Crawler | Class | Discovers |
|---|---|---|
| Sitemap | Sitemap_Crawler | URLs listed in sitemap.xml and sub-sitemaps |
| Taxonomy | Taxonomy_Crawler | Category, tag, and custom taxonomy archive pages |
| Author | Author_Crawler | Author archive pages |
| Post Type Archive | Post_Type_Archive_Crawler | Post type archive URLs (e.g. /portfolio/) |
| Date Archive | Date_Archive_Crawler | Year/month/day archive pages |
| Pagination | Pagination_Crawler | Paginated archive and post pages |
| RSS | Rss_Crawler | RSS feed URLs |
| REST API | Rest_Api_Crawler | REST API endpoint URLs (optional) |
URL Include / Exclude Rules
The Url_Rules engine applies include and exclude rule sets to every discovered URL before it is added to the export queue. This lets you filter out specific paths or restrict exports to a subset of URLs using pattern matching.
Discovery Limits
| Limit | Default | Filter to Override |
|---|---|---|
| Max pages crawled | 50 | url_discovery_max_pages arg |
| Max total URLs | 20,000 | url_discovery_max_urls arg |
Export Engine
§ 10Background Processing Pipeline
The export runs entirely in the background via WordPress Cron (wp_to_html_process_event). The engine uses an adaptive batch system that adjusts the number of URL batches per cron tick based on observed performance.
wp_to_html_export_lock, 45s default) prevents concurrent cron ticks from processing simultaneously. Contended ticks self-heal by rescheduling and nudging WP-Cron.delay = base × 2^(retry-1), capped at a configurable maximum. Default: base 5s, cap 120s for URLs; base 5s, cap 180s for assets.processing state for longer than the TTL threshold (default: 120s for URLs, 180s for assets) and reclaims them back to pending for retry.running, paused, stopped, and completed. Pausing stops background work without corrupting the queue. Cancelling clears the queue and resets to idle.retry_count, last_error, and last_attempt_at timestamp.fetch_urls → fetch_assets → wrapup. The UI receives stage-aware progress so users know whether URLs or assets are currently being processed.ZipArchive). For large exports, files are split into multiple parts (default: 1,000 files/part, configurable via filter). Auto-ZIP is skipped if total work exceeds the configured threshold (default: 5,000 items).Export State Machine
idle ──[Start Export]──► running ──[All done]──► completed
│ │
│ [Pause] ▼
│ paused ──[Resume]──► running
│
└──────────────[Cancel]──► stopped ──► idle
Delivery & Notifications
§ 11FTP / SFTP Upload
The FTP_Uploader class handles direct server-to-server delivery. Configure your FTP settings under Settings → FTP/SFTP.
| Setting | Default | Description |
|---|---|---|
host | — | FTP server hostname or IP address |
port | 21 | FTP port (use 22 for SFTP) |
user | — | FTP username |
pass | — | FTP password (stored in WordPress options; consider wp-config.php constants for hardening) |
ssl | 0 | Enable FTPS (SSL) — requires ftp_ssl_connect() PHP function |
passive | 1 | Use passive mode (recommended for NAT/firewalls) |
timeout | 20 | Connection timeout in seconds (5–120) |
base_path | — | Remote base directory path (must start with /) |
Email Notification
Enable Notify on complete to receive an email when the export finishes. The notification includes the export status, site name, timestamp, and (if created) the ZIP filename.
The initiating admin user always receives the notification. You can add additional recipient email addresses (comma-separated) in the Delivery panel. Emails are sent using wp_mail() — no external service required.
ZIP Download
After export completion, all files are bundled into a ZIP archive automatically (if ZipArchive is available). For large exports:
-part1of3.zip, -part2of3.zip, etc.Export Preview
§ 12After every export, the built-in file browser lets you preview all generated files — HTML pages, images, CSS, and JS — directly inside the WordPress admin panel without leaving the export screen.
From the preview panel you can also download groups of assets as ZIP archives (e.g., all images, all CSS, all HTML files) in a single click. This is especially useful when auto-ZIP is skipped for large exports.
Architecture
§ 13Class Overview
| Class | Namespace | Responsibility |
|---|---|---|
Core | WpToHtml | Bootstraps the plugin, registers cron hooks, manages DB schema upgrades, drives the background processing loop (adaptive batches, watchdog, ZIP creation, email/FTP delivery) |
Admin | WpToHtml | Registers admin menu pages (Tools → Export WP Pages to Static HTML and System Status), enqueues assets, localizes the JS config object |
REST | WpToHtml | Registers all wp_to_html/v1/ REST routes, manages the background runner token, creates/destroys temporary export users for role-based rendering |
Exporter | WpToHtml | Fetches individual URLs, rewrites asset paths, saves HTML files, processes asset batches, logs progress. Reads export context from wp_to_html_export_context option |
Asset_Manager | WpToHtml | Tracks discovered assets in the database, handles deduplication and queuing |
Asset_Extractor | WpToHtml | Parses HTML to extract referenced CSS, JS, and image URLs |
Bulk_Asset_Collector | WpToHtml | Implements Hybrid and Full asset collection modes by enumerating the media library and theme/plugin directories |
FTP_Uploader | WpToHtml | Provides FTP/FTPS connection, upload, and remote directory listing |
Diagnostic | WpToHtml | Runs system health checks (PHP version, ZipArchive, writability, DB permissions, REST API loopback) with 15-minute result caching |
Advanced_Debugger | WpToHtml | Optional diagnostic layer (enabled via WP_TO_HTML_ADVANCED_DEBUG): captures PHP fatal errors, records progress markers, detects stuck exports |
Url_Discovery | WpToHtml\UrlDiscovery | Orchestrates all URL crawlers, applies include/exclude rules, enforces discovery limits |
Url_Rules | WpToHtml\UrlDiscovery | Applies include/exclude URL pattern rules during discovery |
Plugin Constants
| Constant | Value | Description |
|---|---|---|
WP_TO_HTML_VERSION | '1.3.3.5' | Current plugin version |
WP_TO_HTML_PATH | plugin_dir_path() | Absolute filesystem path to the plugin directory |
WP_TO_HTML_URL | plugin_dir_url() | URL to the plugin directory |
WP_TO_HTML_EXPORT_DIR | WP_CONTENT_DIR . '/wp-to-html-exports' | Absolute path to the export output directory |
WP_TO_HTML_DEBUG | false | Enables verbose internal logging to export-log.txt |
WP_TO_HTML_ADVANCED_DEBUG | false | Enables fatal-error capture and stuck-export detection. Set true in wp-config.php |
WP_TO_HTML_PRO_ACTIVE | — | Define as true to signal Pro plugin is active and unlock Pro scopes |
Database Schema
§ 14The plugin creates three custom tables on activation using dbDelta() for safe schema migrations. All tables use the current WordPress table prefix.
pending | processing | done | failedasset | image | css | js | etc.pending | processing | done | failedidle | running | paused | stopped | completedfetch_urls | fetch_assets | wrapupHooks & Filters
§ 15['selected']. Pro adds: 'all_posts', 'all_pages', 'full_site'.Args:
array $scopes
true to signal that the Pro plugin is active. Used as a fallback when neither the WP_TO_HTML_PRO_ACTIVE constant nor the WpToHtml_Pro\Plugin class is found.Args:
bool $active
45 seconds. Lower values reduce the chance of stall after a PHP crash; higher values reduce risk of two ticks running concurrently on slow servers.
2 seconds (minimum: 1).
18.0 seconds. Reduce on servers with tight PHP time limits.
int $target, array $stats, object $status
6.
true.
5000. Set to 0 to always ZIP.
1000. Reduce if ZIP creation fails due to PHP memory limits.
processing is reclaimed by the watchdog. Default: 120.
processing is reclaimed. Default: 180.
3.
3.
wp_to_html_bg_tick_delay_seconds seconds while an export is running.
REST API Reference
§ 16All endpoints are registered under the wp_to_html/v1 namespace and require a valid WordPress nonce (wp_rest). Authentication uses the standard WordPress REST API authentication layer.
| Endpoint | Method | Description |
|---|---|---|
/wp_to_html/v1/export | POST | Start a new export. Accepts scope, selected items, post status, login role, asset options, and delivery settings. |
/wp_to_html/v1/status | GET | Returns current export status including state, progress counters, and pipeline stage. |
/wp_to_html/v1/poll | GET | Lightweight status poll endpoint optimised for frequent UI polling. |
/wp_to_html/v1/log | GET | Returns the current export log entries. |
/wp_to_html/v1/pause | POST | Pause the running export. |
/wp_to_html/v1/resume | POST | Resume a paused export. |
/wp_to_html/v1/stop | POST | Cancel the export and reset to idle. |
/wp_to_html/v1/content | GET | List posts/pages/CPT items for the content picker. |
/wp_to_html/v1/exports | GET | List exported files for the preview panel. |
/wp_to_html/v1/download | GET | Stream a ZIP download of a specific asset group or the full export. |
/wp_to_html/v1/ftp-settings | GET/POST | Read and save FTP/SFTP configuration. |
/wp_to_html/v1/ftp-test | POST | Test FTP connection with current settings. |
/wp_to_html/v1/ftp-list | POST | List remote FTP directory contents. |
/wp_to_html/v1/system-status | GET | Run and return system diagnostics. |
/wp_to_html/v1/check-can-run | GET | Quick preflight check before starting an export. |
/wp_to_html/v1/failed-urls | GET | List all permanently failed URLs with error details. |
/wp_to_html/v1/rerun-failed | POST | Reset failed items to pending and reschedule. |
/wp_to_html/v1/queue-reset | POST | Clear the entire queue and reset status to idle. |
/wp_to_html/v1/clear-temp | POST | Delete any leftover temporary export users. |
/wp_to_html/v1/reset-diagnostics | POST | Clear the cached diagnostics report. |
/wp_to_html/v1/runner | POST | Internal non-blocking background runner endpoint, authenticated by a secret token. |
/wp_to_html/v1/s3-settings | GET/POST | Pro Read and save AWS S3 configuration. |
/wp_to_html/v1/s3-test | POST | Pro Test S3 connection. |
System Diagnostics
§ 17Navigate to Tools → WP to HTML System Status to run a full environment health check. Results are cached for 15 minutes; click Force Refresh to re-run immediately.
| Check | What It Tests | Action if Failing |
|---|---|---|
| PHP & WP Versions | PHP ≥ 7.4, WordPress ≥ 5.8 | Upgrade PHP or WordPress |
| ZipArchive Extension | PHP ZipArchive class is available | Enable the zip extension in php.ini |
| Export Directory Writable | wp-content/wp-to-html-exports/ exists and is writable | Fix file permissions (chmod 755) or create the directory manually |
| Filesystem Method | WordPress can write files without FTP credentials | Set define('FS_METHOD', 'direct'); in wp-config.php |
| Database Permissions | MySQL user can CREATE TABLE, ALTER TABLE, INSERT, UPDATE | Grant additional DB permissions to the WordPress DB user |
| REST API Loopback | WordPress can make HTTP requests to its own REST API | Check firewall rules, WP_HTTP_BLOCK_EXTERNAL constant, or SSL certificate validity |
Advanced Debugging
For difficult-to-diagnose issues, enable the Advanced Debugger by adding to wp-config.php:
define('WP_TO_HTML_ADVANCED_DEBUG', true);
When enabled, the Advanced Debugger registers a register_shutdown_function to capture PHP fatal errors, records timestamped progress markers on each tick, and detects exports that appear stuck (no progress for a configurable threshold, default 120 seconds).
Frequently Asked Questions
§ 18Will running an export affect my live WordPress site?
No. Exports are written to a separate directory (/wp-content/wp-to-html-exports/). Your live WordPress installation remains fully intact and unchanged.
What is the difference between this plugin and Simply Static?
Simply Static and most full-site generators convert your entire WordPress site. This plugin is designed for precision: you choose exactly which posts, pages, or CPT items to export, and in what status and user-role context. This makes it better suited for client deliveries, partial static exports, and content archiving.
Can I export draft or private posts?
Yes. All five post statuses are supported: Publish, Draft, Private, Pending, and Scheduled. Select the desired statuses in the Post Status section of the export panel.
An export seems stuck — what should I do?
First, check the System Status page for any failed health checks, especially the REST API Loopback. If WP-Cron is not running reliably, configure a server-side cron job. If items are stuck in processing, the watchdog will reclaim them automatically on the next tick (within 120–180 seconds). You can also use the Re-run failed button to retry all failed items, or use the Queue Reset endpoint to start fresh.
Does it work on WordPress Multisite?
The free plugin works on individual sites within a multisite network. Each site in the network has its own export directory and database tables (using the site-specific table prefix).
Where are exports stored?
Exports are written to wp-content/wp-to-html-exports/. This directory is created automatically on plugin activation. The export log is stored as export-log.txt in the same directory and is excluded from ZIP archives.
How do I clear exported files?
Use the Queue Reset button in the export panel, or delete the contents of wp-content/wp-to-html-exports/ manually via FTP or the server's file manager. Deactivating the plugin also drops the database tables but preserves the export files.
Can I hook into the export process?
Yes. Several WordPress filters let you customise batch sizes, retry counts, ZIP behaviour, and more. See the Hooks & Filters section for the full list.
Pro Features
§ 19| Feature | Description |
|---|---|
| All Posts Export | Export every post (or selected custom post types) in one run across all configured statuses. |
| All Pages Export | Export your entire pages library with one click. |
| Full Site Export | Complete WordPress-to-static-HTML conversion: all public post types, archives, and discovered URLs. |
| AWS S3 Deployment | Push exports directly to an Amazon S3 bucket. Configure bucket name, region, and credentials. Registered as the wp_to_html/v1/s3-settings and wp_to_html/v1/s3-test REST endpoints. |
| Priority Support | Email support and priority bug fixes. |
The Pro plugin integrates seamlessly by either defining the WP_TO_HTML_PRO_ACTIVE constant, loading the WpToHtml_Pro\Plugin class, or returning true on the wp_to_html/pro_active filter. No modifications to the free plugin core are required.
Changelog
§ 20single_root_index and root_parent_html options now persisted in export context