First Commit

This commit is contained in:
2026-03-18 14:21:32 -07:00
parent 8712bbcc1a
commit 1cf5bbeeb0
12 changed files with 4185 additions and 0 deletions
+267
View File
@@ -0,0 +1,267 @@
/**
* WP to HTML Admin Styles
*/
.wp-to-html-admin-container {
display: flex;
gap: 20px;
margin-top: 20px;
}
.wp-to-html-main {
flex: 1;
background: #fff;
padding: 20px;
border: 1px solid #c3c4c7;
border-radius: 4px;
}
.wp-to-html-sidebar {
width: 350px;
flex-shrink: 0;
}
.wp-to-html-card {
background: #fff;
padding: 20px;
border: 1px solid #c3c4c7;
border-radius: 4px;
margin-bottom: 20px;
}
.wp-to-html-card h3 {
margin-top: 0;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.wp-to-html-bulk-actions {
display: flex;
flex-direction: column;
gap: 10px;
}
.wp-to-html-bulk-actions .button {
text-align: center;
}
.wp-to-html-progress-bar {
height: 20px;
background: #f0f0f1;
border-radius: 10px;
overflow: hidden;
margin: 15px 0;
}
.wp-to-html-progress-fill {
height: 100%;
background: linear-gradient(90deg, #2271b1, #135e96);
width: 0%;
transition: width 0.3s ease;
border-radius: 10px;
}
.wp-to-html-progress-text {
text-align: center;
color: #50575e;
font-size: 13px;
margin: 0;
}
.wp-to-html-stats {
width: 100%;
border-collapse: collapse;
}
.wp-to-html-stats th,
.wp-to-html-stats td {
padding: 8px 0;
text-align: left;
border-bottom: 1px solid #f0f0f1;
}
.wp-to-html-stats th {
color: #50575e;
font-weight: 400;
}
.wp-to-html-stats td {
color: #1d2327;
}
.wp-to-html-stats code {
font-size: 12px;
word-break: break-all;
}
.wp-to-html-code {
background: #f6f7f7;
padding: 15px;
border-radius: 4px;
font-size: 11px;
line-height: 1.5;
overflow-x: auto;
white-space: pre;
font-family: Consolas, Monaco, 'Andale Mono', monospace;
}
#wp-to-html-results {
margin-top: 15px;
padding: 15px;
background: #f6f7f7;
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
}
#wp-to-html-results .result-summary {
display: flex;
gap: 20px;
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #ddd;
}
#wp-to-html-results .result-stat {
text-align: center;
}
#wp-to-html-results .result-stat .number {
font-size: 24px;
font-weight: bold;
display: block;
}
#wp-to-html-results .result-stat.converted .number {
color: #00a32a;
}
#wp-to-html-results .result-stat.skipped .number {
color: #dba617;
}
#wp-to-html-results .result-stat.errors .number {
color: #d63638;
}
#wp-to-html-results .result-details {
font-size: 12px;
}
#wp-to-html-results .result-item {
padding: 5px 0;
border-bottom: 1px solid #eee;
display: flex;
gap: 10px;
align-items: center;
}
#wp-to-html-results .result-item:last-child {
border-bottom: none;
}
#wp-to-html-results .result-item .dashicons {
font-size: 16px;
width: 16px;
height: 16px;
}
#wp-to-html-results .result-item.converted .dashicons {
color: #00a32a;
}
#wp-to-html-results .result-item.skipped .dashicons {
color: #dba617;
}
#wp-to-html-results .result-item.error .dashicons {
color: #d63638;
}
/* Meta box styles */
.wp-to-html-meta-box hr {
margin: 12px 0;
border: 0;
border-top: 1px solid #ddd;
}
.wp-to-html-meta-box .description {
color: #646970;
font-style: italic;
margin-top: 5px;
}
.wp-to-html-status {
margin: 0;
line-height: 1.6;
}
/* Responsive */
@media screen and (max-width: 1200px) {
.wp-to-html-admin-container {
flex-direction: column;
}
.wp-to-html-sidebar {
width: 100%;
}
}
/* Regeneration Status Card */
#wp-to-html-live-status {
min-height: 50px;
}
#wp-to-html-live-status .dashicons {
font-size: 20px;
width: 20px;
height: 20px;
vertical-align: middle;
margin-right: 8px;
}
#wp-to-html-live-status .status-text {
font-weight: 500;
vertical-align: middle;
}
#wp-to-html-live-status .status-source {
color: #646970;
font-size: 12px;
margin-left: 5px;
}
#wp-to-html-live-status .status-progress,
#wp-to-html-live-status .status-details {
margin-top: 8px;
padding-left: 28px;
color: #646970;
font-size: 13px;
}
.status-running .dashicons {
color: #2271b1;
}
.status-complete .dashicons {
color: #00a32a;
}
.status-idle .dashicons {
color: #72777c;
}
/* Spinning animation */
.dashicons.spin {
animation: spin 1.5s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
+378
View File
@@ -0,0 +1,378 @@
/**
* WP to HTML Admin JavaScript
*/
(function ($) {
'use strict';
var WPToHTML = {
// State
isProcessing: false,
totalPosts: 0,
processedPosts: 0,
results: {
converted: 0,
skipped: 0,
errors: 0,
details: []
},
statusPollInterval: null,
/**
* Initialize
*/
init: function () {
this.bindEvents();
this.startStatusPolling();
},
/**
* Bind event handlers
*/
bindEvents: function () {
$('#wp-to-html-convert-all').on('click', $.proxy(this.startBulkConvert, this));
$('#wp-to-html-clear-cache').on('click', $.proxy(this.clearCache, this));
},
/**
* Start status polling
*/
startStatusPolling: function () {
var self = this;
// Check status immediately
this.checkStatus();
// Poll every 3 seconds
this.statusPollInterval = setInterval(function () {
self.checkStatus();
}, 3000);
},
/**
* Stop status polling
*/
stopStatusPolling: function () {
if (this.statusPollInterval) {
clearInterval(this.statusPollInterval);
this.statusPollInterval = null;
}
},
/**
* Check regeneration status
*/
checkStatus: function () {
var self = this;
$.ajax({
url: wpToHtml.ajaxUrl,
type: 'POST',
data: {
action: 'wp_to_html_get_status',
nonce: wpToHtml.statusNonce
},
success: function (response) {
if (response.success) {
self.updateStatusCard(response.data);
}
}
});
},
/**
* Update the status card UI
*/
updateStatusCard: function (status) {
var $container = $('#wp-to-html-live-status');
if (!$container.length) return;
var html = '';
var strings = wpToHtml.strings;
// Get source label
var sourceLabels = {
'cron': strings.sourceCron,
'admin_bar': strings.sourceAdminBar,
'plugin_update': strings.sourcePluginUpdate,
'settings_page': strings.sourceSettingsPage
};
var sourceLabel = status.source ? (sourceLabels[status.source] || '') : '';
if (status.status === 'running' || status.status === 'pending') {
var statusText = status.status === 'pending' ? strings.statusPending : strings.statusRunning;
html = '<div class="status-running">';
html += '<span class="dashicons dashicons-update spin"></span>';
html += '<span class="status-text">' + statusText + '</span>';
if (sourceLabel) {
html += '<span class="status-source">' + sourceLabel + '</span>';
}
if (status.total && status.total > 0) {
html += '<div class="status-progress">' + status.processed + ' / ' + status.total + ' pages processed</div>';
}
html += '</div>';
} else if (status.status === 'complete') {
html = '<div class="status-complete">';
html += '<span class="dashicons dashicons-yes-alt"></span>';
html += '<span class="status-text">' + strings.statusComplete + '</span>';
html += '<div class="status-details">' + status.converted + ' converted, ' + status.skipped + ' skipped, ' + status.errors + ' errors</div>';
html += '</div>';
} else {
// idle
html = '<div class="status-idle">';
html += '<span class="dashicons dashicons-clock"></span>';
html += '<span class="status-text">' + strings.statusIdle + '</span>';
html += '</div>';
}
$container.html(html);
},
/**
* Start bulk conversion
*/
startBulkConvert: function (e) {
e.preventDefault();
if (this.isProcessing) {
return;
}
this.isProcessing = true;
this.processedPosts = 0;
this.results = {
converted: 0,
skipped: 0,
errors: 0,
details: []
};
// Disable buttons
$('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', true);
// Show progress bar
$('#wp-to-html-progress').show();
$('#wp-to-html-results').hide();
this.updateProgress(0, wpToHtml.strings.processing);
// Get total posts count first
this.getTotalPosts();
},
/**
* Get total posts count
*/
getTotalPosts: function () {
var self = this;
$.ajax({
url: wpToHtml.ajaxUrl,
type: 'POST',
data: {
action: 'wp_to_html_get_posts_count',
nonce: wpToHtml.nonce
},
success: function (response) {
if (response.success) {
self.totalPosts = response.data.total;
self.processBatch(0);
} else {
self.handleError(response.data);
}
},
error: function () {
self.handleError(wpToHtml.strings.error);
}
});
},
/**
* Process a batch of posts
*/
processBatch: function (offset) {
var self = this;
$.ajax({
url: wpToHtml.ajaxUrl,
type: 'POST',
data: {
action: 'wp_to_html_bulk_convert',
nonce: wpToHtml.nonce,
offset: offset
},
success: function (response) {
if (response.success) {
// Update results
self.processedPosts += response.data.processed;
self.results.converted += response.data.converted;
self.results.skipped += response.data.skipped;
self.results.errors += response.data.errors;
self.results.details = self.results.details.concat(response.data.details);
// Update progress
var percent = self.totalPosts > 0
? Math.round((self.processedPosts / self.totalPosts) * 100)
: 100;
self.updateProgress(percent, self.processedPosts + ' / ' + self.totalPosts);
// Continue or finish
if (response.data.has_more) {
self.processBatch(response.data.next_offset);
} else {
self.finishBulkConvert();
}
} else {
self.handleError(response.data);
}
},
error: function () {
self.handleError(wpToHtml.strings.error);
}
});
},
/**
* Finish bulk conversion
*/
finishBulkConvert: function () {
this.isProcessing = false;
// Update progress to complete
this.updateProgress(100, wpToHtml.strings.complete);
// Show results
this.showResults();
// Re-enable buttons
$('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', false);
// Refresh status immediately
this.checkStatus();
},
/**
* Update progress bar
*/
updateProgress: function (percent, text) {
$('.wp-to-html-progress-fill').css('width', percent + '%');
$('.wp-to-html-progress-text').text(text);
},
/**
* Show results
*/
showResults: function () {
var html = '<div class="result-summary">';
html += '<div class="result-stat converted"><span class="number">' + this.results.converted + '</span><span class="label">Converted</span></div>';
html += '<div class="result-stat skipped"><span class="number">' + this.results.skipped + '</span><span class="label">Skipped</span></div>';
html += '<div class="result-stat errors"><span class="number">' + this.results.errors + '</span><span class="label">Errors</span></div>';
html += '</div>';
if (this.results.details.length > 0) {
html += '<div class="result-details">';
// Show up to 50 items
var items = this.results.details.slice(0, 50);
for (var i = 0; i < items.length; i++) {
var item = items[i];
var icon = 'yes-alt';
var statusClass = item.status;
if (item.status === 'skipped') {
icon = 'warning';
} else if (item.status === 'error') {
icon = 'dismiss';
}
html += '<div class="result-item ' + statusClass + '">';
html += '<span class="dashicons dashicons-' + icon + '"></span>';
html += '<strong>' + this.escapeHtml(item.title) + '</strong>';
html += '<span class="message">' + this.escapeHtml(item.message) + '</span>';
html += '</div>';
}
if (this.results.details.length > 50) {
html += '<p><em>Showing first 50 results...</em></p>';
}
html += '</div>';
}
$('#wp-to-html-results').html(html).show();
},
/**
* Clear cache
*/
clearCache: function (e) {
e.preventDefault();
if (this.isProcessing) {
return;
}
if (!confirm(wpToHtml.strings.confirmClear)) {
return;
}
this.isProcessing = true;
$('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', true);
var self = this;
$.ajax({
url: wpToHtml.ajaxUrl,
type: 'POST',
data: {
action: 'wp_to_html_clear_cache',
nonce: wpToHtml.nonce
},
success: function (response) {
self.isProcessing = false;
$('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', false);
if (response.success) {
alert(response.data.message);
location.reload();
} else {
self.handleError(response.data);
}
},
error: function () {
self.isProcessing = false;
$('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', false);
self.handleError(wpToHtml.strings.error);
}
});
},
/**
* Handle error
*/
handleError: function (message) {
this.isProcessing = false;
$('#wp-to-html-convert-all, #wp-to-html-clear-cache').prop('disabled', false);
$('#wp-to-html-progress').hide();
alert(message);
},
/**
* Escape HTML
*/
escapeHtml: function (text) {
var div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
};
// Initialize on document ready
$(document).ready(function () {
WPToHTML.init();
});
})(jQuery);
+680
View File
@@ -0,0 +1,680 @@
<?php
/**
* Admin Class
*
* Handles the admin settings page and meta box for manual exclusion.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Admin {
/**
* Single instance
*/
private static $instance = null;
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_init', array($this, 'register_settings'));
add_action('add_meta_boxes', array($this, 'add_meta_box'));
add_action('save_post', array($this, 'save_meta_box'), 10, 2);
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets'));
// AJAX endpoint for regeneration status
add_action('wp_ajax_wp_to_html_get_status', array($this, 'ajax_get_status'));
}
/**
* Add admin menu
*/
public function add_admin_menu() {
add_options_page(
__('WP to HTML Settings', 'wp-to-html'),
__('WP to HTML', 'wp-to-html'),
'manage_options',
'wp-to-html',
array($this, 'render_settings_page')
);
}
/**
* Register settings
*/
public function register_settings() {
register_setting('wp_to_html_settings', 'wp_to_html_settings', array($this, 'sanitize_settings'));
// General Settings Section
add_settings_section(
'wp_to_html_general',
__('General Settings', 'wp-to-html'),
array($this, 'render_general_section'),
'wp-to-html'
);
add_settings_field(
'enabled',
__('Enable Static HTML Generation', 'wp-to-html'),
array($this, 'render_enabled_field'),
'wp-to-html',
'wp_to_html_general'
);
// Exclusion Settings Section
add_settings_section(
'wp_to_html_exclusions',
__('Exclusion Settings', 'wp-to-html'),
array($this, 'render_exclusions_section'),
'wp-to-html'
);
add_settings_field(
'excluded_shortcodes',
__('Excluded Shortcodes', 'wp-to-html'),
array($this, 'render_shortcodes_field'),
'wp-to-html',
'wp_to_html_exclusions'
);
add_settings_field(
'excluded_blocks',
__('Excluded Blocks', 'wp-to-html'),
array($this, 'render_blocks_field'),
'wp-to-html',
'wp_to_html_exclusions'
);
// Cron Settings Section
add_settings_section(
'wp_to_html_cron',
__('Scheduled Regeneration', 'wp-to-html'),
array($this, 'render_cron_section'),
'wp-to-html'
);
add_settings_field(
'cron_interval',
__('Regeneration Schedule', 'wp-to-html'),
array($this, 'render_cron_field'),
'wp-to-html',
'wp_to_html_cron'
);
// Performance Settings Section
add_settings_section(
'wp_to_html_performance',
__('Performance Settings', 'wp-to-html'),
array($this, 'render_performance_section'),
'wp-to-html'
);
add_settings_field(
'bundle_css',
__('Bundle CSS Files', 'wp-to-html'),
array($this, 'render_bundle_css_field'),
'wp-to-html',
'wp_to_html_performance'
);
add_settings_field(
'bundle_js',
__('Bundle JavaScript Files', 'wp-to-html'),
array($this, 'render_bundle_js_field'),
'wp-to-html',
'wp_to_html_performance'
);
// Cache Info Section
add_settings_section(
'wp_to_html_cache',
__('Cache Information', 'wp-to-html'),
array($this, 'render_cache_section'),
'wp-to-html'
);
}
/**
* Sanitize settings
*/
public function sanitize_settings($input) {
$sanitized = array();
$sanitized['enabled'] = isset($input['enabled']) ? (bool) $input['enabled'] : false;
$sanitized['excluded_shortcodes'] = isset($input['excluded_shortcodes'])
? sanitize_text_field($input['excluded_shortcodes'])
: '';
$sanitized['excluded_blocks'] = isset($input['excluded_blocks'])
? sanitize_text_field($input['excluded_blocks'])
: '';
// Sanitize cron interval
$valid_intervals = array('disabled', 'wp_to_html_hourly', 'wp_to_html_twicedaily', 'daily');
$sanitized['cron_interval'] = isset($input['cron_interval']) && in_array($input['cron_interval'], $valid_intervals)
? $input['cron_interval']
: 'wp_to_html_hourly';
// Sanitize bundling options
$sanitized['bundle_css'] = isset($input['bundle_css']) ? (bool) $input['bundle_css'] : false;
$sanitized['bundle_js'] = isset($input['bundle_js']) ? (bool) $input['bundle_js'] : false;
// Reschedule cron if interval changed
$current_settings = WP_To_HTML::get_settings();
if (!isset($current_settings['cron_interval']) || $current_settings['cron_interval'] !== $sanitized['cron_interval']) {
WP_To_HTML_Cron::reschedule_cron($sanitized['cron_interval']);
}
return $sanitized;
}
/**
* Enqueue admin assets
*/
public function enqueue_admin_assets($hook) {
if ($hook === 'settings_page_wp-to-html') {
wp_enqueue_style(
'wp-to-html-admin',
WP_TO_HTML_PLUGIN_URL . 'admin/css/admin.css',
array(),
WP_TO_HTML_VERSION
);
wp_enqueue_script(
'wp-to-html-admin',
WP_TO_HTML_PLUGIN_URL . 'admin/js/admin.js',
array('jquery'),
WP_TO_HTML_VERSION,
true
);
wp_localize_script('wp-to-html-admin', 'wpToHtml', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wp_to_html_bulk'),
'statusNonce' => wp_create_nonce('wp_to_html_status'),
'strings' => array(
'processing' => __('Processing...', 'wp-to-html'),
'complete' => __('Complete!', 'wp-to-html'),
'error' => __('An error occurred', 'wp-to-html'),
'confirmClear' => __('Are you sure you want to clear all cached HTML files?', 'wp-to-html'),
'statusIdle' => __('No regeneration in progress', 'wp-to-html'),
'statusRunning' => __('Regeneration in progress...', 'wp-to-html'),
'statusPending' => __('Waiting to start...', 'wp-to-html'),
'statusComplete' => __('Regeneration complete', 'wp-to-html'),
'sourceCron' => __('(Scheduled Task)', 'wp-to-html'),
'sourceAdminBar' => __('(Admin Bar)', 'wp-to-html'),
'sourcePluginUpdate' => __('(Plugin Update)', 'wp-to-html'),
'sourceSettingsPage' => __('(Settings Page)', 'wp-to-html'),
),
));
}
}
/**
* Render settings page
*/
public function render_settings_page() {
if (!current_user_can('manage_options')) {
return;
}
// Check if cache was just cleared
if (isset($_GET['cache_cleared']) && $_GET['cache_cleared'] === '1') {
add_settings_error('wp_to_html', 'cache_cleared', __('Cache cleared successfully.', 'wp-to-html'), 'success');
}
?>
<div class="wrap">
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
<?php settings_errors('wp_to_html'); ?>
<div class="wp-to-html-admin-container">
<div class="wp-to-html-main">
<form action="options.php" method="post">
<?php
settings_fields('wp_to_html_settings');
do_settings_sections('wp-to-html');
submit_button(__('Save Settings', 'wp-to-html'));
?>
</form>
</div>
<div class="wp-to-html-sidebar">
<div class="wp-to-html-card">
<h3><?php _e('Bulk Actions', 'wp-to-html'); ?></h3>
<p><?php _e('Generate or clear cached HTML files in bulk.', 'wp-to-html'); ?></p>
<div class="wp-to-html-bulk-actions">
<button type="button" class="button button-primary" id="wp-to-html-convert-all">
<?php _e('Convert All Pages & Posts', 'wp-to-html'); ?>
</button>
<button type="button" class="button button-secondary" id="wp-to-html-clear-cache">
<?php _e('Clear All Cache', 'wp-to-html'); ?>
</button>
</div>
<div id="wp-to-html-progress" style="display: none;">
<div class="wp-to-html-progress-bar">
<div class="wp-to-html-progress-fill"></div>
</div>
<p class="wp-to-html-progress-text"></p>
</div>
<div id="wp-to-html-results" style="display: none;"></div>
</div>
<div class="wp-to-html-card" id="wp-to-html-status-card">
<h3><?php _e('Regeneration Status', 'wp-to-html'); ?></h3>
<div id="wp-to-html-live-status">
<?php
$status = WP_To_HTML_Cron::get_regeneration_status();
if ($status && in_array($status['status'], array('running', 'pending'))):
?>
<div class="status-running">
<span class="dashicons dashicons-update spin"></span>
<span class="status-text">
<?php echo $status['status'] === 'pending'
? __('Waiting to start...', 'wp-to-html')
: __('Regeneration in progress...', 'wp-to-html'); ?>
</span>
<?php if (isset($status['source'])): ?>
<span class="status-source">
<?php
$sources = array(
'cron' => __('(Scheduled Task)', 'wp-to-html'),
'admin_bar' => __('(Admin Bar)', 'wp-to-html'),
'plugin_update' => __('(Plugin Update)', 'wp-to-html'),
'settings_page' => __('(Settings Page)', 'wp-to-html'),
);
echo isset($sources[$status['source']]) ? $sources[$status['source']] : '';
?>
</span>
<?php endif; ?>
<?php if (isset($status['total']) && $status['total'] > 0): ?>
<div class="status-progress">
<?php printf(__('%d / %d pages processed', 'wp-to-html'), $status['processed'], $status['total']); ?>
</div>
<?php endif; ?>
</div>
<?php elseif ($status && $status['status'] === 'complete'): ?>
<div class="status-complete">
<span class="dashicons dashicons-yes-alt"></span>
<span class="status-text"><?php _e('Last regeneration complete', 'wp-to-html'); ?></span>
<div class="status-details">
<?php printf(__('%d converted, %d skipped, %d errors', 'wp-to-html'),
$status['converted'], $status['skipped'], $status['errors']); ?>
</div>
</div>
<?php else: ?>
<div class="status-idle">
<span class="dashicons dashicons-clock"></span>
<span class="status-text"><?php _e('No regeneration in progress', 'wp-to-html'); ?></span>
</div>
<?php endif; ?>
</div>
</div>
<div class="wp-to-html-card">
<h3><?php _e('Server Configuration', 'wp-to-html'); ?></h3>
<p><?php _e('For Nginx servers, add this to your server configuration:', 'wp-to-html'); ?></p>
<pre class="wp-to-html-code"><?php echo esc_html($this->get_nginx_rules()); ?></pre>
</div>
</div>
</div>
</div>
<?php
}
/**
* Render general section description
*/
public function render_general_section() {
echo '<p>' . __('Configure the main settings for static HTML generation.', 'wp-to-html') . '</p>';
}
/**
* Render enabled field
*/
public function render_enabled_field() {
$settings = WP_To_HTML::get_settings();
?>
<label>
<input type="checkbox" name="wp_to_html_settings[enabled]" value="1"
<?php checked($settings['enabled'], true); ?>>
<?php _e('Enable static HTML generation for pages and posts', 'wp-to-html'); ?>
</label>
<p class="description">
<?php _e('When enabled, pages and posts will be converted to static HTML files for faster loading.', 'wp-to-html'); ?>
</p>
<?php
}
/**
* Render exclusions section description
*/
public function render_exclusions_section() {
echo '<p>' . __('Configure which pages should be excluded from static HTML generation.', 'wp-to-html') . '</p>';
}
/**
* Render shortcodes field
*/
public function render_shortcodes_field() {
$settings = WP_To_HTML::get_settings();
?>
<input type="text" name="wp_to_html_settings[excluded_shortcodes]"
value="<?php echo esc_attr($settings['excluded_shortcodes']); ?>"
class="large-text">
<p class="description">
<?php _e('Comma-separated list of shortcodes. Pages containing these shortcodes will not be converted to static HTML.', 'wp-to-html'); ?>
<br>
<?php _e('Example: contact-form-7, wpforms, gravityform', 'wp-to-html'); ?>
</p>
<?php
}
/**
* Render blocks field
*/
public function render_blocks_field() {
$settings = WP_To_HTML::get_settings();
?>
<input type="text" name="wp_to_html_settings[excluded_blocks]"
value="<?php echo esc_attr($settings['excluded_blocks']); ?>"
class="large-text">
<p class="description">
<?php _e('Comma-separated list of Gutenberg block names or partial names. Pages containing these blocks will not be converted to static HTML.', 'wp-to-html'); ?>
<br>
<?php _e('Example: wpforms, gravityforms, contact-form-7, ninja-forms', 'wp-to-html'); ?>
</p>
<p class="description">
<strong><?php _e('Automatic exclusions:', 'wp-to-html'); ?></strong>
<?php _e('Pages with comments enabled and WooCommerce pages (cart, checkout, my account, products) are automatically excluded.', 'wp-to-html'); ?>
</p>
<?php
}
/**
* Render cron section description
*/
public function render_cron_section() {
echo '<p>' . __('Configure automatic regeneration of stale cached pages.', 'wp-to-html') . '</p>';
}
/**
* Render cron interval field
*/
public function render_cron_field() {
$settings = WP_To_HTML::get_settings();
$current_interval = isset($settings['cron_interval']) ? $settings['cron_interval'] : 'wp_to_html_hourly';
$intervals = array(
'disabled' => __('Disabled', 'wp-to-html'),
'wp_to_html_hourly' => __('Every Hour', 'wp-to-html'),
'wp_to_html_twicedaily' => __('Twice Daily', 'wp-to-html'),
'daily' => __('Daily', 'wp-to-html'),
);
?>
<select name="wp_to_html_settings[cron_interval]">
<?php foreach ($intervals as $value => $label): ?>
<option value="<?php echo esc_attr($value); ?>" <?php selected($current_interval, $value); ?>>
<?php echo esc_html($label); ?>
</option>
<?php endforeach; ?>
</select>
<p class="description">
<?php _e('How often to check for and regenerate stale cached pages. Only pages that have been modified since their last cache will be regenerated.', 'wp-to-html'); ?>
</p>
<?php
// Show next scheduled run
$next_run = WP_To_HTML_Cron::get_next_run();
$last_run = WP_To_HTML_Cron::get_last_run();
if ($next_run): ?>
<p class="description">
<strong><?php _e('Next scheduled run:', 'wp-to-html'); ?></strong>
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $next_run)); ?>
</p>
<?php endif;
if ($last_run): ?>
<p class="description">
<strong><?php _e('Last run:', 'wp-to-html'); ?></strong>
<?php echo esc_html($last_run['time']); ?>
(<?php printf(__('%d pages regenerated', 'wp-to-html'), $last_run['regenerated']); ?>)
</p>
<?php endif;
}
/**
* Render performance section description
*/
public function render_performance_section() {
echo '<p>' . __('Configure performance optimizations for static HTML files.', 'wp-to-html') . '</p>';
}
/**
* Render bundle CSS field
*/
public function render_bundle_css_field() {
$settings = WP_To_HTML::get_settings();
?>
<label>
<input type="checkbox" name="wp_to_html_settings[bundle_css]" value="1"
<?php checked(!empty($settings['bundle_css']), true); ?>>
<?php _e('Combine all CSS files into a single bundled file', 'wp-to-html'); ?>
</label>
<p class="description">
<?php _e('When enabled, all CSS stylesheets will be downloaded and combined into a single file for faster page loading.', 'wp-to-html'); ?>
</p>
<?php
}
/**
* Render bundle JS field
*/
public function render_bundle_js_field() {
$settings = WP_To_HTML::get_settings();
?>
<label>
<input type="checkbox" name="wp_to_html_settings[bundle_js]" value="1"
<?php checked(!empty($settings['bundle_js']), true); ?>>
<?php _e('Combine all JavaScript files into a single bundled file', 'wp-to-html'); ?>
</label>
<p class="description">
<?php _e('When enabled, all JavaScript files will be downloaded and combined into a single file for faster page loading.', 'wp-to-html'); ?>
</p>
<?php
}
/**
* Render cache section
*/
public function render_cache_section() {
$generator = WP_To_HTML_Generator::get_instance();
$stats = $generator->get_cache_stats();
?>
<table class="wp-to-html-stats">
<tr>
<th><?php _e('Cache Directory:', 'wp-to-html'); ?></th>
<td><code><?php echo esc_html(WP_TO_HTML_CACHE_DIR); ?></code></td>
</tr>
<tr>
<th><?php _e('Cached Files:', 'wp-to-html'); ?></th>
<td><?php echo esc_html($stats['total_files']); ?></td>
</tr>
<tr>
<th><?php _e('Total Size:', 'wp-to-html'); ?></th>
<td><?php echo esc_html(size_format($stats['total_size'])); ?></td>
</tr>
<?php if ($stats['oldest_file']): ?>
<tr>
<th><?php _e('Oldest Cache:', 'wp-to-html'); ?></th>
<td><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $stats['oldest_file'])); ?></td>
</tr>
<?php endif; ?>
<?php if ($stats['newest_file']): ?>
<tr>
<th><?php _e('Newest Cache:', 'wp-to-html'); ?></th>
<td><?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $stats['newest_file'])); ?></td>
</tr>
<?php endif; ?>
</table>
<?php
}
/**
* Add meta box to post/page editor
*/
public function add_meta_box() {
$post_types = get_post_types(array('public' => true), 'names');
$excluded = array('attachment', 'product', 'product_variation');
$post_types = array_diff($post_types, $excluded);
foreach ($post_types as $post_type) {
add_meta_box(
'wp_to_html_exclude',
__('WP to HTML', 'wp-to-html'),
array($this, 'render_meta_box'),
$post_type,
'side',
'default'
);
}
}
/**
* Render meta box content
*/
public function render_meta_box($post) {
wp_nonce_field('wp_to_html_meta_box', 'wp_to_html_meta_box_nonce');
$exclude = get_post_meta($post->ID, '_wp_to_html_exclude', true);
$generator = WP_To_HTML_Generator::get_instance();
$has_cache = $generator->has_cache($post->ID);
$exclusion_reason = $generator->should_exclude($post);
?>
<div class="wp-to-html-meta-box">
<p>
<label>
<input type="checkbox" name="wp_to_html_exclude" value="1"
<?php checked($exclude, '1'); ?>>
<?php _e('Keep as dynamic PHP', 'wp-to-html'); ?>
</label>
</p>
<p class="description">
<?php _e('Check this to prevent this page from being converted to static HTML.', 'wp-to-html'); ?>
</p>
<hr>
<p class="wp-to-html-status">
<strong><?php _e('Status:', 'wp-to-html'); ?></strong><br>
<?php if ($exclusion_reason && $exclude !== '1'): ?>
<span class="dashicons dashicons-warning" style="color: #dba617;"></span>
<?php echo esc_html($exclusion_reason); ?>
<?php elseif ($exclude === '1'): ?>
<span class="dashicons dashicons-lock" style="color: #72777c;"></span>
<?php _e('Manually excluded', 'wp-to-html'); ?>
<?php elseif ($has_cache): ?>
<span class="dashicons dashicons-yes-alt" style="color: #00a32a;"></span>
<?php _e('Cached as static HTML', 'wp-to-html'); ?>
<?php else: ?>
<span class="dashicons dashicons-clock" style="color: #72777c;"></span>
<?php _e('Not yet cached', 'wp-to-html'); ?>
<?php endif; ?>
</p>
</div>
<?php
}
/**
* Save meta box data
*/
public function save_meta_box($post_id, $post) {
// Verify nonce
if (!isset($_POST['wp_to_html_meta_box_nonce']) ||
!wp_verify_nonce($_POST['wp_to_html_meta_box_nonce'], 'wp_to_html_meta_box')) {
return;
}
// Check permissions
if (!current_user_can('edit_post', $post_id)) {
return;
}
// Don't save for autosaves
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
// Save the exclusion setting
$exclude = isset($_POST['wp_to_html_exclude']) ? '1' : '';
update_post_meta($post_id, '_wp_to_html_exclude', $exclude);
}
/**
* Get Nginx configuration rules
*/
private function get_nginx_rules() {
$cache_path = str_replace(ABSPATH, '/', WP_TO_HTML_CACHE_DIR);
return "# WP-to-HTML Static Serving
location / {
# Skip for logged-in users
if (\$http_cookie ~* \"wordpress_logged_in\") {
set \$skip_cache 1;
}
# Skip for POST requests
if (\$request_method = POST) {
set \$skip_cache 1;
}
# Try to serve cached file
set \$cache_file \$document_root{$cache_path}\$uri/index.html;
if (-f \$cache_file) {
rewrite ^(.*)\$ {$cache_path}\$uri/index.html break;
}
}";
}
/**
* AJAX handler to get regeneration status
*/
public function ajax_get_status() {
check_ajax_referer('wp_to_html_status', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$status = WP_To_HTML_Cron::get_regeneration_status();
if (!$status) {
wp_send_json_success(array(
'status' => 'idle',
));
}
wp_send_json_success($status);
}
}
+480
View File
@@ -0,0 +1,480 @@
<?php
/**
* Assets Class
*
* Handles bundling of CSS and JavaScript files into single files
* for improved performance of static HTML pages.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Assets {
/**
* Single instance
*/
private static $instance = null;
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Nothing to initialize
}
/**
* Bundle CSS and JS assets in the HTML
*
* @param string $html The HTML content
* @param int $post_id The post ID
* @param string $page_url The original page URL
* @return string Modified HTML with bundled assets
*/
public function bundle_assets($html, $post_id, $page_url) {
$settings = WP_To_HTML::get_settings();
// Bundle CSS if enabled
if (!empty($settings['bundle_css'])) {
$html = $this->bundle_css($html, $post_id, $page_url);
}
// Bundle JS if enabled
if (!empty($settings['bundle_js'])) {
$html = $this->bundle_js($html, $post_id, $page_url);
}
return $html;
}
/**
* Bundle all CSS files into a single file
*
* @param string $html The HTML content
* @param int $post_id The post ID
* @param string $page_url The original page URL
* @return string Modified HTML with bundled CSS
*/
private function bundle_css($html, $post_id, $page_url) {
// Extract all stylesheet links
$stylesheets = $this->extract_stylesheets($html);
if (empty($stylesheets)) {
return $html;
}
$bundled_css = '';
$base_url = $this->get_base_url($page_url);
// Download and combine all CSS files
foreach ($stylesheets as $stylesheet) {
$url = $stylesheet['href'];
$absolute_url = $this->make_absolute_url($url, $base_url);
$css_content = $this->download_asset($absolute_url);
if ($css_content !== false) {
// Rewrite URLs in CSS to be absolute
$css_content = $this->rewrite_css_urls($css_content, $absolute_url);
// Add source comment and content
$bundled_css .= "\n/* Source: {$absolute_url} */\n";
$bundled_css .= $css_content . "\n";
}
}
if (empty($bundled_css)) {
return $html;
}
// Generate a hash for the bundle filename
$bundle_hash = md5($bundled_css);
$bundle_filename = "bundle-{$post_id}-{$bundle_hash}.css";
// Save the bundled CSS file
$assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/';
if (!file_exists($assets_dir)) {
wp_mkdir_p($assets_dir);
}
$bundle_path = $assets_dir . $bundle_filename;
file_put_contents($bundle_path, $bundled_css);
// Get the URL for the bundled file
$bundle_url = WP_TO_HTML_CACHE_URL . 'assets/' . $bundle_filename;
// Remove all original stylesheet links
foreach ($stylesheets as $stylesheet) {
$html = str_replace($stylesheet['full_tag'], '', $html);
}
// Add the bundled CSS link before </head>
$bundled_link = '<link rel="stylesheet" href="' . esc_url($bundle_url) . '" type="text/css" media="all" />';
$html = str_replace('</head>', $bundled_link . "\n</head>", $html);
return $html;
}
/**
* Bundle all JS files into a single file
*
* @param string $html The HTML content
* @param int $post_id The post ID
* @param string $page_url The original page URL
* @return string Modified HTML with bundled JS
*/
private function bundle_js($html, $post_id, $page_url) {
// Extract all script tags with src attributes
$scripts = $this->extract_scripts($html);
if (empty($scripts)) {
return $html;
}
$bundled_js = '';
$base_url = $this->get_base_url($page_url);
$has_defer = false;
$has_async = false;
// Download and combine all JS files
foreach ($scripts as $script) {
$url = $script['src'];
$absolute_url = $this->make_absolute_url($url, $base_url);
$js_content = $this->download_asset($absolute_url);
if ($js_content !== false) {
// Add source comment and content
$bundled_js .= "\n/* Source: {$absolute_url} */\n";
$bundled_js .= $js_content . ";\n";
}
// Track defer/async attributes
if (!empty($script['defer'])) {
$has_defer = true;
}
if (!empty($script['async'])) {
$has_async = true;
}
}
if (empty($bundled_js)) {
return $html;
}
// Generate a hash for the bundle filename
$bundle_hash = md5($bundled_js);
$bundle_filename = "bundle-{$post_id}-{$bundle_hash}.js";
// Save the bundled JS file
$assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/';
if (!file_exists($assets_dir)) {
wp_mkdir_p($assets_dir);
}
$bundle_path = $assets_dir . $bundle_filename;
file_put_contents($bundle_path, $bundled_js);
// Get the URL for the bundled file
$bundle_url = WP_TO_HTML_CACHE_URL . 'assets/' . $bundle_filename;
// Remove all original script tags
foreach ($scripts as $script) {
$html = str_replace($script['full_tag'], '', $html);
}
// Build the bundled script tag with appropriate attributes
$attributes = '';
if ($has_defer) {
$attributes .= ' defer';
}
if ($has_async) {
$attributes .= ' async';
}
// Add the bundled JS script before </body>
$bundled_script = '<script src="' . esc_url($bundle_url) . '"' . $attributes . '></script>';
$html = str_replace('</body>', $bundled_script . "\n</body>", $html);
return $html;
}
/**
* Extract all stylesheet links from HTML
*
* @param string $html The HTML content
* @return array Array of stylesheet data
*/
private function extract_stylesheets($html) {
$stylesheets = array();
// Match link tags with rel="stylesheet"
$pattern = '/<link[^>]*rel=["\']stylesheet["\'][^>]*>/i';
preg_match_all($pattern, $html, $matches);
// Also match link tags where rel comes after href
$pattern2 = '/<link[^>]*href=["\'][^"\']+["\'][^>]*rel=["\']stylesheet["\'][^>]*>/i';
preg_match_all($pattern2, $html, $matches2);
$all_matches = array_unique(array_merge($matches[0], $matches2[0]));
foreach ($all_matches as $tag) {
// Extract href
if (preg_match('/href=["\']([^"\']+)["\']/i', $tag, $href_match)) {
$href = $href_match[1];
// Skip data URIs and inline styles
if (strpos($href, 'data:') === 0) {
continue;
}
$stylesheets[] = array(
'full_tag' => $tag,
'href' => $href,
);
}
}
return $stylesheets;
}
/**
* Extract all script tags with src from HTML
*
* @param string $html The HTML content
* @return array Array of script data
*/
private function extract_scripts($html) {
$scripts = array();
// Match script tags with src attribute
$pattern = '/<script[^>]*src=["\']([^"\']+)["\'][^>]*><\/script>/i';
preg_match_all($pattern, $html, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$tag = $match[0];
$src = $match[1];
// Skip data URIs
if (strpos($src, 'data:') === 0) {
continue;
}
// Check for defer/async attributes
$defer = (stripos($tag, 'defer') !== false);
$async = (stripos($tag, 'async') !== false);
$scripts[] = array(
'full_tag' => $tag,
'src' => $src,
'defer' => $defer,
'async' => $async,
);
}
return $scripts;
}
/**
* Download an asset from a URL
*
* @param string $url The asset URL
* @return string|false The asset content or false on failure
*/
private function download_asset($url) {
$response = wp_remote_get($url, array(
'timeout' => 15,
'sslverify' => false,
));
if (is_wp_error($response)) {
return false;
}
$status_code = wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
return false;
}
return wp_remote_retrieve_body($response);
}
/**
* Rewrite relative URLs in CSS to be absolute
*
* @param string $css The CSS content
* @param string $css_url The original CSS file URL
* @return string CSS with absolute URLs
*/
private function rewrite_css_urls($css, $css_url) {
$base_url = dirname($css_url) . '/';
// Match url() patterns
$pattern = '/url\s*\(\s*["\']?(?!data:)(?!https?:\/\/)(?!\/\/)([^"\'\)]+)["\']?\s*\)/i';
$css = preg_replace_callback($pattern, function($matches) use ($base_url) {
$relative_url = $matches[1];
// Handle ../ paths
if (strpos($relative_url, '../') === 0) {
$absolute_url = $this->resolve_relative_path($base_url, $relative_url);
} elseif (strpos($relative_url, './') === 0) {
$absolute_url = $base_url . substr($relative_url, 2);
} else {
$absolute_url = $base_url . $relative_url;
}
return 'url("' . $absolute_url . '")';
}, $css);
// Handle @import statements
$import_pattern = '/@import\s+["\'](?!data:)(?!https?:\/\/)(?!\/\/)([^"\']+)["\']/i';
$css = preg_replace_callback($import_pattern, function($matches) use ($base_url) {
$relative_url = $matches[1];
$absolute_url = $this->resolve_relative_path($base_url, $relative_url);
return '@import "' . $absolute_url . '"';
}, $css);
return $css;
}
/**
* Resolve a relative path against a base URL
*
* @param string $base_url The base URL
* @param string $relative_path The relative path
* @return string The absolute URL
*/
private function resolve_relative_path($base_url, $relative_path) {
// Parse the base URL
$parsed = parse_url($base_url);
$scheme = isset($parsed['scheme']) ? $parsed['scheme'] . '://' : 'https://';
$host = isset($parsed['host']) ? $parsed['host'] : '';
$path = isset($parsed['path']) ? $parsed['path'] : '/';
// Remove filename from path if exists
if (substr($path, -1) !== '/') {
$path = dirname($path) . '/';
}
// Process the relative path
while (strpos($relative_path, '../') === 0) {
$relative_path = substr($relative_path, 3);
$path = dirname(rtrim($path, '/')) . '/';
}
// Remove leading ./
if (strpos($relative_path, './') === 0) {
$relative_path = substr($relative_path, 2);
}
return $scheme . $host . $path . $relative_path;
}
/**
* Make a URL absolute
*
* @param string $url The URL to make absolute
* @param string $base_url The base URL
* @return string The absolute URL
*/
private function make_absolute_url($url, $base_url) {
// Already absolute
if (preg_match('/^https?:\/\//i', $url)) {
return $url;
}
// Protocol-relative URL
if (strpos($url, '//') === 0) {
$parsed_base = parse_url($base_url);
$scheme = isset($parsed_base['scheme']) ? $parsed_base['scheme'] : 'https';
return $scheme . ':' . $url;
}
// Absolute path (starts with /)
if (strpos($url, '/') === 0) {
$parsed_base = parse_url($base_url);
$scheme = isset($parsed_base['scheme']) ? $parsed_base['scheme'] : 'https';
$host = isset($parsed_base['host']) ? $parsed_base['host'] : '';
return $scheme . '://' . $host . $url;
}
// Relative path
return $this->resolve_relative_path($base_url, $url);
}
/**
* Get the base URL from a page URL
*
* @param string $url The page URL
* @return string The base URL (without path)
*/
private function get_base_url($url) {
$parsed = parse_url($url);
$scheme = isset($parsed['scheme']) ? $parsed['scheme'] : 'https';
$host = isset($parsed['host']) ? $parsed['host'] : '';
$path = isset($parsed['path']) ? $parsed['path'] : '/';
return $scheme . '://' . $host . $path;
}
/**
* Clear bundled assets from cache
*
* @return bool
*/
public function clear_bundled_assets() {
$assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/';
if (!file_exists($assets_dir)) {
return true;
}
$files = glob($assets_dir . 'bundle-*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
return true;
}
/**
* Delete bundled assets for a specific post
*
* @param int $post_id The post ID
* @return bool
*/
public function delete_post_assets($post_id) {
$assets_dir = WP_TO_HTML_CACHE_DIR . 'assets/';
if (!file_exists($assets_dir)) {
return true;
}
$files = glob($assets_dir . "bundle-{$post_id}-*");
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
return true;
}
}
+286
View File
@@ -0,0 +1,286 @@
<?php
/**
* Bulk Conversion Class
*
* Handles bulk conversion of pages and posts to static HTML.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Bulk {
/**
* Single instance
*/
private static $instance = null;
/**
* Batch size for processing
*/
const BATCH_SIZE = 10;
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
add_action('wp_ajax_wp_to_html_bulk_convert', array($this, 'ajax_bulk_convert'));
add_action('wp_ajax_wp_to_html_clear_cache', array($this, 'ajax_clear_cache'));
add_action('wp_ajax_wp_to_html_get_posts_count', array($this, 'ajax_get_posts_count'));
}
/**
* AJAX handler for getting total posts count
*/
public function ajax_get_posts_count() {
check_ajax_referer('wp_to_html_bulk', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$count = $this->get_eligible_posts_count();
// Set initial status for bulk conversion
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'running',
'source' => 'settings_page',
'started' => time(),
'total' => $count,
'processed' => 0,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
));
wp_send_json_success(array(
'total' => $count,
));
}
/**
* AJAX handler for bulk conversion
*/
public function ajax_bulk_convert() {
check_ajax_referer('wp_to_html_bulk', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$offset = isset($_POST['offset']) ? intval($_POST['offset']) : 0;
$result = $this->process_batch($offset);
// Get current status to accumulate totals
$current_status = WP_To_HTML_Cron::get_regeneration_status();
$converted = isset($current_status['converted']) ? $current_status['converted'] : 0;
$skipped = isset($current_status['skipped']) ? $current_status['skipped'] : 0;
$errors = isset($current_status['errors']) ? $current_status['errors'] : 0;
$processed = isset($current_status['processed']) ? $current_status['processed'] : 0;
// Update status with accumulated progress
$new_status = array(
'source' => 'settings_page',
'started' => isset($current_status['started']) ? $current_status['started'] : time(),
'total' => $result['total'],
'processed' => $processed + $result['processed'],
'converted' => $converted + $result['converted'],
'skipped' => $skipped + $result['skipped'],
'errors' => $errors + $result['errors'],
);
if ($result['has_more']) {
$new_status['status'] = 'running';
} else {
$new_status['status'] = 'complete';
$new_status['completed'] = time();
}
WP_To_HTML_Cron::set_regeneration_status($new_status);
wp_send_json_success($result);
}
/**
* AJAX handler for clearing cache
*/
public function ajax_clear_cache() {
check_ajax_referer('wp_to_html_bulk', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$generator = WP_To_HTML_Generator::get_instance();
$generator->clear_all_cache();
wp_send_json_success(array(
'message' => __('Cache cleared successfully', 'wp-to-html'),
));
}
/**
* Get count of eligible posts
*/
private function get_eligible_posts_count() {
$post_types = $this->get_eligible_post_types();
$args = array(
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
);
$query = new WP_Query($args);
return $query->post_count;
}
/**
* Process a batch of posts
*/
private function process_batch($offset) {
$post_types = $this->get_eligible_post_types();
$args = array(
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => self::BATCH_SIZE,
'offset' => $offset,
'orderby' => 'ID',
'order' => 'ASC',
);
$query = new WP_Query($args);
$generator = WP_To_HTML_Generator::get_instance();
$results = array(
'processed' => 0,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
'details' => array(),
'has_more' => false,
'next_offset' => $offset + self::BATCH_SIZE,
);
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$post_id = get_the_ID();
$post_title = get_the_title();
$results['processed']++;
$generation_result = $generator->generate($post_id);
if (is_wp_error($generation_result)) {
if ($generation_result->get_error_code() === 'excluded') {
$results['skipped']++;
$results['details'][] = array(
'id' => $post_id,
'title' => $post_title,
'status' => 'skipped',
'message' => $generation_result->get_error_message(),
);
} else {
$results['errors']++;
$results['details'][] = array(
'id' => $post_id,
'title' => $post_title,
'status' => 'error',
'message' => $generation_result->get_error_message(),
);
}
} else {
$results['converted']++;
$results['details'][] = array(
'id' => $post_id,
'title' => $post_title,
'status' => 'converted',
'message' => __('Successfully converted', 'wp-to-html'),
);
}
}
wp_reset_postdata();
}
// Check if there are more posts
$total = $this->get_eligible_posts_count();
$results['has_more'] = ($offset + self::BATCH_SIZE) < $total;
$results['total'] = $total;
return $results;
}
/**
* Get eligible post types
*/
private function get_eligible_post_types() {
$post_types = get_post_types(array('public' => true), 'names');
$excluded = array('attachment', 'product', 'product_variation');
return array_values(array_diff($post_types, $excluded));
}
/**
* Bulk convert all eligible posts (CLI method)
*/
public function convert_all() {
$post_types = $this->get_eligible_post_types();
$args = array(
'post_type' => $post_types,
'post_status' => 'publish',
'posts_per_page' => -1,
);
$query = new WP_Query($args);
$generator = WP_To_HTML_Generator::get_instance();
$results = array(
'total' => $query->post_count,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
);
if ($query->have_posts()) {
while ($query->have_posts()) {
$query->the_post();
$post_id = get_the_ID();
$generation_result = $generator->generate($post_id);
if (is_wp_error($generation_result)) {
if ($generation_result->get_error_code() === 'excluded') {
$results['skipped']++;
} else {
$results['errors']++;
}
} else {
$results['converted']++;
}
}
wp_reset_postdata();
}
return $results;
}
}
+270
View File
@@ -0,0 +1,270 @@
<?php
/**
* Cron Class
*
* Handles scheduled cache regeneration for stale pages.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Cron {
/**
* Single instance
*/
private static $instance = null;
/**
* Cron hook name
*/
const CRON_HOOK = 'wp_to_html_scheduled_regeneration';
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Register the cron action
add_action(self::CRON_HOOK, array($this, 'run_scheduled_regeneration'));
// Add custom cron schedules
add_filter('cron_schedules', array($this, 'add_cron_schedules'));
}
/**
* Add custom cron schedules
*
* @param array $schedules Existing schedules
* @return array Modified schedules
*/
public function add_cron_schedules($schedules) {
$schedules['wp_to_html_hourly'] = array(
'interval' => HOUR_IN_SECONDS,
'display' => __('Every Hour (WP-to-HTML)', 'wp-to-html'),
);
$schedules['wp_to_html_twicedaily'] = array(
'interval' => 12 * HOUR_IN_SECONDS,
'display' => __('Twice Daily (WP-to-HTML)', 'wp-to-html'),
);
return $schedules;
}
/**
* Schedule the cron event
*/
public static function schedule_cron() {
$settings = WP_To_HTML::get_settings();
$interval = isset($settings['cron_interval']) ? $settings['cron_interval'] : 'wp_to_html_hourly';
// Don't schedule if disabled
if ($interval === 'disabled') {
self::unschedule_cron();
return;
}
// Only schedule if not already scheduled
if (!wp_next_scheduled(self::CRON_HOOK)) {
wp_schedule_event(time(), $interval, self::CRON_HOOK);
}
}
/**
* Unschedule the cron event
*/
public static function unschedule_cron() {
$timestamp = wp_next_scheduled(self::CRON_HOOK);
if ($timestamp) {
wp_unschedule_event($timestamp, self::CRON_HOOK);
}
// Clear all events with this hook
wp_clear_scheduled_hook(self::CRON_HOOK);
}
/**
* Reschedule cron with new interval
*
* @param string $interval New interval
*/
public static function reschedule_cron($interval) {
self::unschedule_cron();
if ($interval !== 'disabled') {
wp_schedule_event(time(), $interval, self::CRON_HOOK);
}
}
/**
* Run the scheduled regeneration
*/
public function run_scheduled_regeneration() {
$settings = WP_To_HTML::get_settings();
// Check if plugin is enabled
if (!$settings['enabled']) {
return;
}
$generator = WP_To_HTML_Generator::get_instance();
$stale_posts = $this->get_stale_posts();
$total = count($stale_posts);
// Set initial status
self::set_regeneration_status(array(
'status' => 'running',
'source' => 'cron',
'started' => time(),
'total' => $total,
'processed' => 0,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
));
$converted = 0;
$errors = 0;
foreach ($stale_posts as $index => $post_id) {
$result = $generator->generate($post_id);
if (is_wp_error($result)) {
$errors++;
} else {
$converted++;
}
// Update status periodically (every 5 pages or at the end)
if (($index + 1) % 5 === 0 || ($index + 1) === $total) {
self::set_regeneration_status(array(
'status' => 'running',
'source' => 'cron',
'started' => time(),
'total' => $total,
'processed' => $index + 1,
'converted' => $converted,
'skipped' => 0,
'errors' => $errors,
));
}
}
// Mark complete
self::set_regeneration_status(array(
'status' => 'complete',
'source' => 'cron',
'completed' => time(),
'total' => $total,
'processed' => $total,
'converted' => $converted,
'skipped' => 0,
'errors' => $errors,
));
// Log the regeneration
update_option('wp_to_html_last_cron_run', array(
'time' => current_time('mysql'),
'timestamp' => time(),
'regenerated' => $converted,
));
}
/**
* Set regeneration status transient
*
* @param array $status Status data
*/
public static function set_regeneration_status($status) {
set_transient('wp_to_html_regeneration_status', $status, 5 * MINUTE_IN_SECONDS);
}
/**
* Get regeneration status
*
* @return array|false Status data or false if not set
*/
public static function get_regeneration_status() {
return get_transient('wp_to_html_regeneration_status');
}
/**
* Clear regeneration status
*/
public static function clear_regeneration_status() {
delete_transient('wp_to_html_regeneration_status');
}
/**
* Get posts that need regeneration
*
* @return array Array of post IDs
*/
private function get_stale_posts() {
$stale_posts = array();
$metadata_dir = WP_TO_HTML_CACHE_DIR . '.metadata/';
if (!file_exists($metadata_dir)) {
return $stale_posts;
}
// Scan metadata directory for cached posts
$metadata_files = glob($metadata_dir . '*.json');
foreach ($metadata_files as $metadata_file) {
$metadata = json_decode(file_get_contents($metadata_file), true);
if (!$metadata || !isset($metadata['post_id'])) {
continue;
}
$post_id = $metadata['post_id'];
$post = get_post($post_id);
// Skip if post no longer exists
if (!$post) {
continue;
}
// Check if post was modified after cache was generated
$post_modified = strtotime($post->post_modified);
$cache_generated = isset($metadata['generated_timestamp']) ? $metadata['generated_timestamp'] : 0;
if ($post_modified > $cache_generated) {
$stale_posts[] = $post_id;
}
}
return $stale_posts;
}
/**
* Get the next scheduled run time
*
* @return int|false Timestamp or false if not scheduled
*/
public static function get_next_run() {
return wp_next_scheduled(self::CRON_HOOK);
}
/**
* Get the last cron run info
*
* @return array|false
*/
public static function get_last_run() {
return get_option('wp_to_html_last_cron_run', false);
}
}
+472
View File
@@ -0,0 +1,472 @@
<?php
/**
* HTML Generator Class
*
* Handles the generation of static HTML files from WordPress pages and posts.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Generator {
/**
* Single instance
*/
private static $instance = null;
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Nothing to initialize
}
/**
* Generate static HTML for a post/page
*
* @param int $post_id The post ID
* @return bool|WP_Error True on success, WP_Error on failure
*/
public function generate($post_id) {
$post = get_post($post_id);
if (!$post) {
return new WP_Error('invalid_post', __('Invalid post ID', 'wp-to-html'));
}
// Check if generation is enabled
$settings = WP_To_HTML::get_settings();
if (!$settings['enabled']) {
return new WP_Error('disabled', __('Static HTML generation is disabled', 'wp-to-html'));
}
// Check exclusions
$exclusion_reason = $this->should_exclude($post);
if ($exclusion_reason) {
return new WP_Error('excluded', $exclusion_reason);
}
// Get the permalink
$url = get_permalink($post_id);
if (!$url) {
return new WP_Error('no_permalink', __('Could not get permalink for post', 'wp-to-html'));
}
// Fetch the rendered page
$response = wp_remote_get($url, array(
'timeout' => 30,
'sslverify' => false,
'cookies' => array(), // No cookies to get the logged-out version
));
if (is_wp_error($response)) {
return $response;
}
$html = wp_remote_retrieve_body($response);
$status_code = wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
return new WP_Error('bad_response', sprintf(__('Received HTTP %d response', 'wp-to-html'), $status_code));
}
if (empty($html)) {
return new WP_Error('empty_response', __('Received empty response', 'wp-to-html'));
}
// Get the cache path for this URL
$cache_path = $this->get_cache_path($url);
// Create directory if it doesn't exist
$cache_dir = dirname($cache_path);
if (!file_exists($cache_dir)) {
wp_mkdir_p($cache_dir);
}
// Bundle CSS and JS assets if enabled
$assets = WP_To_HTML_Assets::get_instance();
$html = $assets->bundle_assets($html, $post_id, $url);
// Add cache generation comment to HTML
$timestamp = current_time('mysql');
$cache_comment = "\n<!-- Generated by WP-to-HTML on {$timestamp} -->";
$html = preg_replace('/<\/html>/i', $cache_comment . "\n</html>", $html);
// Save the HTML file
$result = file_put_contents($cache_path, $html);
if ($result === false) {
return new WP_Error('write_failed', __('Failed to write cache file', 'wp-to-html'));
}
// Save metadata
$this->save_metadata($post_id, $url, $cache_path);
return true;
}
/**
* Check if a post should be excluded from static generation
*
* @param WP_Post $post The post object
* @return string|false Exclusion reason or false if not excluded
*/
public function should_exclude($post) {
// Check if post is published
if ($post->post_status !== 'publish') {
return __('Post is not published', 'wp-to-html');
}
// Check manual exclusion meta
$manual_exclude = get_post_meta($post->ID, '_wp_to_html_exclude', true);
if ($manual_exclude === '1' || $manual_exclude === 'yes') {
return __('Manually excluded via post meta', 'wp-to-html');
}
// Check if comments are open
if (comments_open($post->ID)) {
return __('Comments are enabled on this post', 'wp-to-html');
}
// Check for excluded shortcodes
$settings = WP_To_HTML::get_settings();
$excluded_shortcodes = array_map('trim', explode(',', $settings['excluded_shortcodes']));
foreach ($excluded_shortcodes as $shortcode) {
if (!empty($shortcode) && has_shortcode($post->post_content, $shortcode)) {
return sprintf(__('Contains excluded shortcode: %s', 'wp-to-html'), $shortcode);
}
}
// Check for excluded blocks (Gutenberg)
$excluded_block = $this->has_excluded_block($post, $settings);
if ($excluded_block) {
return sprintf(__('Contains excluded block: %s', 'wp-to-html'), $excluded_block);
}
// Check for WooCommerce
if ($this->is_woocommerce_page($post)) {
return __('WooCommerce page (dynamic content required)', 'wp-to-html');
}
return false;
}
/**
* Check if a post contains any excluded Gutenberg blocks
*
* @param WP_Post $post The post object
* @param array $settings Plugin settings
* @return string|false Block name if found, false otherwise
*/
private function has_excluded_block($post, $settings) {
// Get excluded blocks from settings
$excluded_blocks_setting = isset($settings['excluded_blocks']) ? $settings['excluded_blocks'] : '';
$excluded_blocks = array_filter(array_map('trim', explode(',', $excluded_blocks_setting)));
if (empty($excluded_blocks)) {
return false;
}
// Parse blocks from content
if (!function_exists('parse_blocks')) {
return false; // Gutenberg not available
}
$blocks = parse_blocks($post->post_content);
// Recursively check all blocks
return $this->find_excluded_block($blocks, $excluded_blocks);
}
/**
* Recursively search for excluded blocks
*
* @param array $blocks Array of parsed blocks
* @param array $excluded_blocks Array of excluded block names
* @return string|false Block name if found, false otherwise
*/
private function find_excluded_block($blocks, $excluded_blocks) {
foreach ($blocks as $block) {
$block_name = isset($block['blockName']) ? $block['blockName'] : '';
// Check if this block is excluded
foreach ($excluded_blocks as $excluded) {
if (!empty($excluded) && !empty($block_name)) {
// Support both full block names (core/form) and partial matches (wpforms)
if (stripos($block_name, $excluded) !== false) {
return $block_name;
}
}
}
// Check inner blocks recursively
if (!empty($block['innerBlocks'])) {
$found = $this->find_excluded_block($block['innerBlocks'], $excluded_blocks);
if ($found) {
return $found;
}
}
}
return false;
}
/**
* Check if a post is a WooCommerce page that should be excluded
*
* @param WP_Post $post The post object
* @return bool
*/
private function is_woocommerce_page($post) {
// Check if WooCommerce is active
if (!class_exists('WooCommerce')) {
return false;
}
// Check if it's a product
if ($post->post_type === 'product') {
return true;
}
// Check WooCommerce core pages
$woo_pages = array(
wc_get_page_id('cart'),
wc_get_page_id('checkout'),
wc_get_page_id('myaccount'),
wc_get_page_id('shop'),
);
if (in_array($post->ID, $woo_pages)) {
return true;
}
return false;
}
/**
* Get the cache file path for a URL
*
* @param string $url The page URL
* @return string The cache file path
*/
public function get_cache_path($url) {
$parsed = parse_url($url);
$path = isset($parsed['path']) ? $parsed['path'] : '/';
// Remove leading and trailing slashes
$path = trim($path, '/');
// Handle homepage
if (empty($path)) {
return WP_TO_HTML_CACHE_DIR . 'index.html';
}
// Create path with index.html
return WP_TO_HTML_CACHE_DIR . $path . '/index.html';
}
/**
* Save metadata for a cached file
*
* @param int $post_id The post ID
* @param string $url The original URL
* @param string $cache_path The cache file path
*/
private function save_metadata($post_id, $url, $cache_path) {
$metadata_dir = WP_TO_HTML_CACHE_DIR . '.metadata/';
if (!file_exists($metadata_dir)) {
wp_mkdir_p($metadata_dir);
}
$metadata = array(
'post_id' => $post_id,
'url' => $url,
'cache_path' => $cache_path,
'generated_at' => current_time('mysql'),
'generated_timestamp' => time(),
);
$metadata_file = $metadata_dir . $post_id . '.json';
file_put_contents($metadata_file, json_encode($metadata, JSON_PRETTY_PRINT));
}
/**
* Delete cached file for a post
*
* @param int $post_id The post ID
* @return bool
*/
public function delete_cache($post_id) {
$post = get_post($post_id);
if (!$post) {
return false;
}
$url = get_permalink($post_id);
$cache_path = $this->get_cache_path($url);
// Delete the HTML file
if (file_exists($cache_path)) {
unlink($cache_path);
}
// Delete bundled assets for this post
$assets = WP_To_HTML_Assets::get_instance();
$assets->delete_post_assets($post_id);
// Delete metadata
$metadata_file = WP_TO_HTML_CACHE_DIR . '.metadata/' . $post_id . '.json';
if (file_exists($metadata_file)) {
unlink($metadata_file);
}
// Try to remove empty directories
$this->cleanup_empty_dirs(dirname($cache_path));
return true;
}
/**
* Clear entire cache
*
* @return bool
*/
public function clear_all_cache() {
if (!file_exists(WP_TO_HTML_CACHE_DIR)) {
return true;
}
$this->recursive_delete(WP_TO_HTML_CACHE_DIR, true);
// Recreate the cache directory structure
wp_mkdir_p(WP_TO_HTML_CACHE_DIR);
wp_mkdir_p(WP_TO_HTML_CACHE_DIR . '.metadata/');
wp_mkdir_p(WP_TO_HTML_CACHE_DIR . 'assets/');
// Add index.php for security
file_put_contents(WP_TO_HTML_CACHE_DIR . 'index.php', "<?php\n// Silence is golden.");
return true;
}
/**
* Recursively delete directory contents
*
* @param string $dir Directory path
* @param bool $keep_root Whether to keep the root directory
*/
private function recursive_delete($dir, $keep_root = false) {
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), array('.', '..'));
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
$this->recursive_delete($path);
} else {
unlink($path);
}
}
if (!$keep_root) {
rmdir($dir);
}
}
/**
* Cleanup empty directories
*
* @param string $dir Directory path
*/
private function cleanup_empty_dirs($dir) {
// Don't delete the main cache directory
if ($dir === WP_TO_HTML_CACHE_DIR || $dir === rtrim(WP_TO_HTML_CACHE_DIR, '/')) {
return;
}
if (is_dir($dir) && count(glob($dir . '/*')) === 0) {
rmdir($dir);
$this->cleanup_empty_dirs(dirname($dir));
}
}
/**
* Get stats about the cache
*
* @return array Cache statistics
*/
public function get_cache_stats() {
$stats = array(
'total_files' => 0,
'total_size' => 0,
'oldest_file' => null,
'newest_file' => null,
);
if (!file_exists(WP_TO_HTML_CACHE_DIR)) {
return $stats;
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(WP_TO_HTML_CACHE_DIR, RecursiveDirectoryIterator::SKIP_DOTS)
);
foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'html') {
$stats['total_files']++;
$stats['total_size'] += $file->getSize();
$mtime = $file->getMTime();
if ($stats['oldest_file'] === null || $mtime < $stats['oldest_file']) {
$stats['oldest_file'] = $mtime;
}
if ($stats['newest_file'] === null || $mtime > $stats['newest_file']) {
$stats['newest_file'] = $mtime;
}
}
}
return $stats;
}
/**
* Check if a post has a cached version
*
* @param int $post_id The post ID
* @return bool
*/
public function has_cache($post_id) {
$post = get_post($post_id);
if (!$post) {
return false;
}
$url = get_permalink($post_id);
$cache_path = $this->get_cache_path($url);
return file_exists($cache_path);
}
}
+785
View File
@@ -0,0 +1,785 @@
<?php
/**
* Hooks Class
*
* Handles WordPress hooks for automatic cache regeneration.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Hooks {
/**
* Single instance
*/
private static $instance = null;
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize all hooks
*/
private function init_hooks() {
// Post/page update hooks
add_action('save_post', array($this, 'on_save_post'), 20, 3);
add_action('delete_post', array($this, 'on_delete_post'), 10);
add_action('trash_post', array($this, 'on_delete_post'), 10);
add_action('transition_post_status', array($this, 'on_status_change'), 10, 3);
// Comment hooks
add_action('comment_post', array($this, 'on_comment_change'), 10, 2);
add_action('edit_comment', array($this, 'on_comment_edit'), 10);
add_action('delete_comment', array($this, 'on_comment_delete'), 10);
add_action('wp_set_comment_status', array($this, 'on_comment_status_change'), 10, 2);
// Global change hooks - clear entire cache
add_action('switch_theme', array($this, 'on_global_change'));
add_action('customize_save_after', array($this, 'on_global_change'));
add_action('update_option_sidebars_widgets', array($this, 'on_global_change'));
add_action('wp_update_nav_menu', array($this, 'on_global_change'));
// Plugin/theme update hooks - regenerate after other plugins update, skip for this plugin
add_action('upgrader_process_complete', array($this, 'on_plugin_update'), 10, 2);
// WooCommerce hooks (if active)
add_action('woocommerce_product_set_stock', array($this, 'on_woo_product_change'));
add_action('woocommerce_variation_set_stock', array($this, 'on_woo_product_change'));
// Admin bar indicator for cache status
add_action('admin_bar_menu', array($this, 'add_admin_bar_indicator'), 100);
add_action('wp_head', array($this, 'admin_bar_styles'));
add_action('admin_head', array($this, 'admin_bar_styles'));
add_action('wp_footer', array($this, 'admin_bar_scripts'));
add_action('admin_footer', array($this, 'admin_bar_scripts'));
// AJAX handlers for admin bar actions
add_action('wp_ajax_wp_to_html_adminbar_clear', array($this, 'ajax_adminbar_clear'));
add_action('wp_ajax_wp_to_html_adminbar_regenerate', array($this, 'ajax_adminbar_regenerate'));
add_action('wp_ajax_wp_to_html_adminbar_regenerate_page', array($this, 'ajax_adminbar_regenerate_page'));
add_action('wp_ajax_wp_to_html_adminbar_clear_regenerate', array($this, 'ajax_adminbar_clear_regenerate'));
}
/**
* Handle post save
*
* @param int $post_id Post ID
* @param WP_Post $post Post object
* @param bool $update Whether this is an update
*/
public function on_save_post($post_id, $post, $update) {
// Don't process revisions
if (wp_is_post_revision($post_id)) {
return;
}
// Don't process autosaves
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
// Only process pages and posts (and custom post types that are public)
$allowed_types = $this->get_allowed_post_types();
if (!in_array($post->post_type, $allowed_types)) {
return;
}
$generator = WP_To_HTML_Generator::get_instance();
// If the post is published, regenerate the cache
if ($post->post_status === 'publish') {
// Schedule regeneration to run after all other save operations
wp_schedule_single_event(time() + 5, 'wp_to_html_regenerate_post', array($post_id));
} else {
// Post is not published, delete the cache
$generator->delete_cache($post_id);
}
}
/**
* Handle post deletion
*
* @param int $post_id Post ID
*/
public function on_delete_post($post_id) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($post_id);
}
/**
* Handle post status transitions
*
* @param string $new_status New status
* @param string $old_status Old status
* @param WP_Post $post Post object
*/
public function on_status_change($new_status, $old_status, $post) {
$generator = WP_To_HTML_Generator::get_instance();
// If unpublishing, delete cache
if ($old_status === 'publish' && $new_status !== 'publish') {
$generator->delete_cache($post->ID);
}
// If publishing, generate cache
if ($new_status === 'publish' && $old_status !== 'publish') {
wp_schedule_single_event(time() + 5, 'wp_to_html_regenerate_post', array($post->ID));
}
}
/**
* Handle new comment
*
* @param int $comment_id Comment ID
* @param int|string $comment_approved Approval status
*/
public function on_comment_change($comment_id, $comment_approved) {
$comment = get_comment($comment_id);
if ($comment) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($comment->comment_post_ID);
}
}
/**
* Handle comment edit
*
* @param int $comment_id Comment ID
*/
public function on_comment_edit($comment_id) {
$comment = get_comment($comment_id);
if ($comment) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($comment->comment_post_ID);
}
}
/**
* Handle comment deletion
*
* @param int $comment_id Comment ID
*/
public function on_comment_delete($comment_id) {
$comment = get_comment($comment_id);
if ($comment) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($comment->comment_post_ID);
}
}
/**
* Handle comment status change
*
* @param int $comment_id Comment ID
* @param string $status New status
*/
public function on_comment_status_change($comment_id, $status) {
$comment = get_comment($comment_id);
if ($comment) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($comment->comment_post_ID);
}
}
/**
* Handle global changes that affect all pages
*/
public function on_global_change() {
$generator = WP_To_HTML_Generator::get_instance();
$generator->clear_all_cache();
}
/**
* Handle plugin/theme updates
*
* Regenerates cache when other plugins are updated, but skips when this plugin is updated
*
* @param WP_Upgrader $upgrader Upgrader instance
* @param array $options Update options including type and plugins
*/
public function on_plugin_update($upgrader, $options) {
// Only handle plugin updates
if (!isset($options['type']) || $options['type'] !== 'plugin') {
return;
}
// Get the list of plugins being updated
$plugins = array();
if (isset($options['plugins'])) {
$plugins = (array) $options['plugins'];
} elseif (isset($options['plugin'])) {
$plugins = array($options['plugin']);
}
// Check if our plugin is in the update list
$our_plugin = 'WP-to-HTML/wp-to-html.php';
$our_plugin_alt = plugin_basename(WP_TO_HTML_PLUGIN_DIR . 'wp-to-html.php');
foreach ($plugins as $plugin) {
if ($plugin === $our_plugin || $plugin === $our_plugin_alt) {
// This plugin is being updated - don't clear or regenerate
return;
}
}
// Other plugins are being updated - clear cache and regenerate
$generator = WP_To_HTML_Generator::get_instance();
$generator->clear_all_cache();
// Set pending status for plugin update regeneration
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'pending',
'source' => 'plugin_update',
'started' => time(),
'message' => __('Waiting for plugin update to complete...', 'wp-to-html'),
));
// Schedule regeneration to run after the update completes
wp_schedule_single_event(time() + 10, 'wp_to_html_regenerate_all', array('plugin_update'));
}
/**
* Handle WooCommerce product changes
*
* @param WC_Product $product Product object
*/
public function on_woo_product_change($product) {
// Products are excluded by default, but clear cache just in case
if (is_object($product) && method_exists($product, 'get_id')) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->delete_cache($product->get_id());
}
}
/**
* Get allowed post types for static generation
*
* @return array List of post type slugs
*/
private function get_allowed_post_types() {
$post_types = get_post_types(array(
'public' => true,
), 'names');
// Remove excluded types
$excluded = array('attachment', 'product', 'product_variation');
return array_diff($post_types, $excluded);
}
/**
* Add admin bar indicator for cache status
*
* @param WP_Admin_Bar $admin_bar Admin bar instance
*/
public function add_admin_bar_indicator($admin_bar) {
// Only show for users who can manage options
if (!current_user_can('manage_options')) {
return;
}
$settings = WP_To_HTML::get_settings();
$generator = WP_To_HTML_Generator::get_instance();
// Determine if we're on the frontend viewing a singular page
$is_frontend_singular = !is_admin() && is_singular();
if ($is_frontend_singular) {
// Frontend: Show page-specific status and regenerate option
$post_id = get_the_ID();
if (!$post_id) {
return;
}
$post = get_post($post_id);
// Determine cache status
$exclusion_reason = $generator->should_exclude($post);
$has_cache = $generator->has_cache($post_id);
if (!$settings['enabled']) {
$status = 'disabled';
$icon = '⏸';
$title = __('WP-to-HTML: Disabled', 'wp-to-html');
$color = '#72777c';
} elseif ($exclusion_reason) {
$status = 'excluded';
$icon = '⚠';
$title = sprintf(__('WP-to-HTML: Excluded - %s', 'wp-to-html'), $exclusion_reason);
$color = '#dba617';
} elseif ($has_cache) {
$status = 'cached';
$icon = '⚡';
$title = __('WP-to-HTML: Cached (Static HTML)', 'wp-to-html');
$color = '#00a32a';
} else {
$status = 'not-cached';
$icon = '○';
$title = __('WP-to-HTML: Not Cached Yet', 'wp-to-html');
$color = '#72777c';
}
$admin_bar->add_node(array(
'id' => 'wp-to-html-status',
'title' => '<span class="wp-to-html-indicator" style="color: ' . esc_attr($color) . ';" title="' . esc_attr($title) . '">' . $icon . '</span> <span class="wp-to-html-label">' . esc_html($title) . '</span>',
'href' => admin_url('options-general.php?page=wp-to-html'),
'meta' => array(
'class' => 'wp-to-html-admin-bar-item wp-to-html-status-' . $status,
),
));
// Add submenu items for frontend
$admin_bar->add_node(array(
'id' => 'wp-to-html-settings',
'parent' => 'wp-to-html-status',
'title' => __('⚙️ Settings', 'wp-to-html'),
'href' => admin_url('options-general.php?page=wp-to-html'),
));
// Only show regenerate if not excluded
if (!$exclusion_reason && $settings['enabled']) {
$admin_bar->add_node(array(
'id' => 'wp-to-html-regenerate-page',
'parent' => 'wp-to-html-status',
'title' => __('🔄 Regenerate This Page', 'wp-to-html'),
'href' => '#',
'meta' => array(
'class' => 'wp-to-html-action-regenerate-page',
'onclick' => 'return false;',
'data-post-id' => $post_id,
),
));
}
} else if (is_admin()) {
// Backend: Show global actions
$stats = $generator->get_cache_stats();
$icon = '⚡';
// Don't show "(0 cached)" - only show count if there are cached pages
if ($stats['total_files'] > 0) {
$title = sprintf(__('WP-to-HTML (%d cached)', 'wp-to-html'), $stats['total_files']);
} else {
$title = __('WP-to-HTML', 'wp-to-html');
}
$admin_bar->add_node(array(
'id' => 'wp-to-html-status',
'title' => '<span class="wp-to-html-indicator">' . $icon . '</span> <span class="wp-to-html-label">' . esc_html($title) . '</span>',
'href' => admin_url('options-general.php?page=wp-to-html'),
'meta' => array(
'class' => 'wp-to-html-admin-bar-item',
),
));
// Add submenu items for backend
$admin_bar->add_node(array(
'id' => 'wp-to-html-settings',
'parent' => 'wp-to-html-status',
'title' => __('⚙️ Settings', 'wp-to-html'),
'href' => admin_url('options-general.php?page=wp-to-html'),
));
$admin_bar->add_node(array(
'id' => 'wp-to-html-clear',
'parent' => 'wp-to-html-status',
'title' => __('🗑️ Clear All Cache', 'wp-to-html'),
'href' => '#',
'meta' => array(
'class' => 'wp-to-html-action-clear',
'onclick' => 'return false;',
),
));
$admin_bar->add_node(array(
'id' => 'wp-to-html-regenerate',
'parent' => 'wp-to-html-status',
'title' => __('🔄 Regenerate All', 'wp-to-html'),
'href' => '#',
'meta' => array(
'class' => 'wp-to-html-action-regenerate',
'onclick' => 'return false;',
),
));
$admin_bar->add_node(array(
'id' => 'wp-to-html-clear-regenerate',
'parent' => 'wp-to-html-status',
'title' => __('✨ Clear & Regenerate All', 'wp-to-html'),
'href' => '#',
'meta' => array(
'class' => 'wp-to-html-action-clear-regenerate',
'onclick' => 'return false;',
),
));
}
}
/**
* Add admin bar styles for the cache indicator
*/
public function admin_bar_styles() {
if (!is_admin_bar_showing() || !current_user_can('manage_options')) {
return;
}
?>
<style>
#wpadminbar .wp-to-html-indicator {
font-size: 16px;
margin-right: 4px;
}
#wpadminbar .wp-to-html-label {
font-size: 13px;
}
@media screen and (max-width: 782px) {
#wpadminbar .wp-to-html-label {
display: none;
}
}
#wpadminbar .wp-to-html-action-clear:hover,
#wpadminbar .wp-to-html-action-regenerate:hover {
cursor: pointer;
}
#wpadminbar .wp-to-html-processing .ab-item {
opacity: 0.6;
pointer-events: none;
}
</style>
<?php
}
/**
* Add admin bar scripts for AJAX actions
*/
public function admin_bar_scripts() {
if (!is_admin_bar_showing() || !current_user_can('manage_options')) {
return;
}
?>
<script>
(function() {
document.addEventListener('DOMContentLoaded', function() {
var clearBtn = document.querySelector('#wp-admin-bar-wp-to-html-clear .ab-item');
var regenBtn = document.querySelector('#wp-admin-bar-wp-to-html-regenerate .ab-item');
var regenPageBtn = document.querySelector('#wp-admin-bar-wp-to-html-regenerate-page .ab-item');
var nonce = '<?php echo wp_create_nonce('wp_to_html_adminbar'); ?>';
var ajaxUrl = '<?php echo admin_url('admin-ajax.php'); ?>';
if (clearBtn) {
clearBtn.addEventListener('click', function(e) {
e.preventDefault();
if (!confirm('<?php echo esc_js(__('Are you sure you want to clear all cached HTML files?', 'wp-to-html')); ?>')) {
return;
}
var parent = this.parentElement;
parent.classList.add('wp-to-html-processing');
this.textContent = '<?php echo esc_js(__('Clearing...', 'wp-to-html')); ?>';
fetch(ajaxUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=wp_to_html_adminbar_clear&nonce=' + nonce
})
.then(function(response) { return response.json(); })
.then(function(data) {
parent.classList.remove('wp-to-html-processing');
if (data.success) {
clearBtn.textContent = '<?php echo esc_js(__('✓ Cache Cleared!', 'wp-to-html')); ?>';
setTimeout(function() { location.reload(); }, 1000);
} else {
clearBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
}
})
.catch(function() {
parent.classList.remove('wp-to-html-processing');
clearBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
});
});
}
if (regenBtn) {
regenBtn.addEventListener('click', function(e) {
e.preventDefault();
if (!confirm('<?php echo esc_js(__('This will regenerate all eligible pages. Continue?', 'wp-to-html')); ?>')) {
return;
}
var parent = this.parentElement;
parent.classList.add('wp-to-html-processing');
this.textContent = '<?php echo esc_js(__('Regenerating...', 'wp-to-html')); ?>';
fetch(ajaxUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=wp_to_html_adminbar_regenerate&nonce=' + nonce
})
.then(function(response) { return response.json(); })
.then(function(data) {
parent.classList.remove('wp-to-html-processing');
if (data.success) {
regenBtn.textContent = '<?php echo esc_js(__('✓ Regenerated!', 'wp-to-html')); ?> (' + data.data.converted + ' pages)';
setTimeout(function() { location.reload(); }, 1500);
} else {
regenBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
}
})
.catch(function() {
parent.classList.remove('wp-to-html-processing');
regenBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
});
});
}
if (regenPageBtn) {
regenPageBtn.addEventListener('click', function(e) {
e.preventDefault();
var parent = this.parentElement;
var postId = parent.querySelector('[data-post-id]');
if (!postId) {
// Try to get post ID from the li element's data attribute
postId = document.querySelector('#wp-admin-bar-wp-to-html-regenerate-page');
}
// Extract post ID - it's stored in the meta
var postIdValue = postId ? postId.getAttribute('data-post-id') : null;
if (!postIdValue) {
// Fallback: get from current page URL or use a hidden input
postIdValue = '<?php echo is_singular() ? get_the_ID() : 0; ?>';
}
parent.classList.add('wp-to-html-processing');
this.textContent = '<?php echo esc_js(__('Regenerating...', 'wp-to-html')); ?>';
fetch(ajaxUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=wp_to_html_adminbar_regenerate_page&nonce=' + nonce + '&post_id=' + postIdValue
})
.then(function(response) { return response.json(); })
.then(function(data) {
parent.classList.remove('wp-to-html-processing');
if (data.success) {
regenPageBtn.textContent = '<?php echo esc_js(__('✓ Page Regenerated!', 'wp-to-html')); ?>';
setTimeout(function() { location.reload(); }, 1000);
} else {
regenPageBtn.textContent = data.data || '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
}
})
.catch(function() {
parent.classList.remove('wp-to-html-processing');
regenPageBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
});
});
}
// Clear & Regenerate All button
var clearRegenBtn = document.querySelector('#wp-admin-bar-wp-to-html-clear-regenerate .ab-item');
if (clearRegenBtn) {
clearRegenBtn.addEventListener('click', function(e) {
e.preventDefault();
if (!confirm('<?php echo esc_js(__('This will clear all cache and regenerate all eligible pages. Continue?', 'wp-to-html')); ?>')) {
return;
}
var parent = this.parentElement;
parent.classList.add('wp-to-html-processing');
this.textContent = '<?php echo esc_js(__('Clearing and Regenerating...', 'wp-to-html')); ?>';
fetch(ajaxUrl, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'action=wp_to_html_adminbar_clear_regenerate&nonce=' + nonce
})
.then(function(response) { return response.json(); })
.then(function(data) {
parent.classList.remove('wp-to-html-processing');
if (data.success) {
clearRegenBtn.textContent = '<?php echo esc_js(__('✓ Done!', 'wp-to-html')); ?> (' + data.data.converted + ' pages)';
setTimeout(function() { location.reload(); }, 1500);
} else {
clearRegenBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
}
})
.catch(function() {
parent.classList.remove('wp-to-html-processing');
clearRegenBtn.textContent = '<?php echo esc_js(__('Error occurred', 'wp-to-html')); ?>';
});
});
}
});
})();
</script>
<?php
}
/**
* AJAX handler for clearing cache from admin bar
*/
public function ajax_adminbar_clear() {
check_ajax_referer('wp_to_html_adminbar', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$generator = WP_To_HTML_Generator::get_instance();
$generator->clear_all_cache();
wp_send_json_success(array(
'message' => __('Cache cleared successfully', 'wp-to-html'),
));
}
/**
* AJAX handler for regenerating all from admin bar
*/
public function ajax_adminbar_regenerate() {
check_ajax_referer('wp_to_html_adminbar', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
// Set initial status
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'running',
'source' => 'admin_bar',
'started' => time(),
'total' => 0,
'processed' => 0,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
));
$bulk = WP_To_HTML_Bulk::get_instance();
$results = $bulk->convert_all();
// Set complete status
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'complete',
'source' => 'admin_bar',
'completed' => time(),
'total' => $results['total'],
'processed' => $results['total'],
'converted' => $results['converted'],
'skipped' => $results['skipped'],
'errors' => $results['errors'],
));
wp_send_json_success(array(
'message' => __('Regeneration complete', 'wp-to-html'),
'converted' => $results['converted'],
'skipped' => $results['skipped'],
'errors' => $results['errors'],
));
}
/**
* AJAX handler for regenerating a single page from admin bar
*/
public function ajax_adminbar_regenerate_page() {
check_ajax_referer('wp_to_html_adminbar', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
$post_id = isset($_POST['post_id']) ? intval($_POST['post_id']) : 0;
if (!$post_id) {
wp_send_json_error(__('Invalid post ID', 'wp-to-html'));
}
$generator = WP_To_HTML_Generator::get_instance();
$result = $generator->generate($post_id);
if (is_wp_error($result)) {
wp_send_json_error($result->get_error_message());
}
wp_send_json_success(array(
'message' => __('Page regenerated successfully', 'wp-to-html'),
));
}
/**
* AJAX handler for clearing cache and regenerating all from admin bar
*/
public function ajax_adminbar_clear_regenerate() {
check_ajax_referer('wp_to_html_adminbar', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Permission denied', 'wp-to-html'));
}
// First clear the cache
$generator = WP_To_HTML_Generator::get_instance();
$generator->clear_all_cache();
// Then regenerate all
$bulk = WP_To_HTML_Bulk::get_instance();
$results = $bulk->convert_all();
wp_send_json_success(array(
'message' => __('Cache cleared and regeneration complete', 'wp-to-html'),
'converted' => $results['converted'],
'skipped' => $results['skipped'],
'errors' => $results['errors'],
));
}
}
// Register the scheduled event handlers
add_action('wp_to_html_regenerate_post', function($post_id) {
$generator = WP_To_HTML_Generator::get_instance();
$generator->generate($post_id);
});
// Register handler for regenerating all pages after plugin updates
add_action('wp_to_html_regenerate_all', function($source = 'plugin_update') {
// Set running status
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'running',
'source' => $source,
'started' => time(),
'total' => 0,
'processed' => 0,
'converted' => 0,
'skipped' => 0,
'errors' => 0,
));
$bulk = WP_To_HTML_Bulk::get_instance();
$results = $bulk->convert_all();
// Set complete status
WP_To_HTML_Cron::set_regeneration_status(array(
'status' => 'complete',
'source' => $source,
'completed' => time(),
'total' => $results['total'],
'processed' => $results['total'],
'converted' => $results['converted'],
'skipped' => $results['skipped'],
'errors' => $results['errors'],
));
});
+138
View File
@@ -0,0 +1,138 @@
<?php
/**
* Server Class
*
* Handles serving static HTML files as a PHP fallback when .htaccess rules don't work.
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
class WP_To_HTML_Server {
/**
* Single instance
*/
private static $instance = null;
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
// Hook early to serve cached content before WordPress runs
add_action('template_redirect', array($this, 'maybe_serve_cached'), 0);
}
/**
* Check if we should serve a cached file and do so if appropriate
*/
public function maybe_serve_cached() {
// Check if generation is enabled
$settings = WP_To_HTML::get_settings();
if (!$settings['enabled']) {
return;
}
// Don't serve cached content for logged-in users
if (is_user_logged_in()) {
return;
}
// Don't serve for POST requests
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
return;
}
// Don't serve for requests with query strings
if (!empty($_SERVER['QUERY_STRING'])) {
return;
}
// Don't serve for admin pages
if (is_admin()) {
return;
}
// Don't serve for AJAX requests
if (wp_doing_ajax()) {
return;
}
// Don't serve for REST API requests
if (defined('REST_REQUEST') && REST_REQUEST) {
return;
}
// Get the current request URI
$request_uri = $_SERVER['REQUEST_URI'];
// Remove leading slash and get cache path
$path = trim(parse_url($request_uri, PHP_URL_PATH), '/');
if (empty($path)) {
$cache_file = WP_TO_HTML_CACHE_DIR . 'index.html';
} else {
$cache_file = WP_TO_HTML_CACHE_DIR . $path . '/index.html';
}
// Check if cached file exists
if (!file_exists($cache_file)) {
return;
}
// Serve the cached file
$this->serve_file($cache_file);
}
/**
* Serve a cached HTML file
*
* @param string $file_path Path to the cached file
*/
private function serve_file($file_path) {
// Set headers
header('Content-Type: text/html; charset=utf-8');
header('X-WP-To-HTML: served');
header('Cache-Control: public, max-age=3600');
// Get file modification time for caching headers
$mtime = filemtime($file_path);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
// Handle conditional requests
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
$if_modified = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
if ($if_modified >= $mtime) {
header('HTTP/1.1 304 Not Modified');
exit;
}
}
// Output the file
readfile($file_path);
exit;
}
/**
* Check if current request is being served from cache
*
* @return bool
*/
public static function is_serving_from_cache() {
return isset($_SERVER['HTTP_X_WP_TO_HTML']) ||
(function_exists('apache_request_headers') &&
isset(apache_request_headers()['X-WP-To-HTML']));
}
}
+98
View File
@@ -0,0 +1,98 @@
=== WP to HTML ===
Contributors: yourname
Tags: static html, performance, speed, cache, optimization
Requires at least: 5.0
Tested up to: 6.4
Requires PHP: 7.2
Stable tag: 0.2.4
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Converts WordPress pages and posts to static HTML files for dramatically faster page loading.
== Description ==
WP to HTML is a performance optimization plugin that converts your WordPress pages and posts into static HTML files. When visitors access your site, they receive pre-rendered HTML instead of waiting for PHP to process each request, resulting in significantly faster page load times.
**Key Features:**
* **Automatic HTML Generation** - Pages and posts are automatically converted to static HTML when published or updated
* **Scheduled Regeneration** - Cron job automatically regenerates stale cached pages
* **Smart Exclusion Rules** - Automatically excludes pages with forms, comments, or WooCommerce functionality
* **Manual Exclusion** - Checkbox in the editor to keep specific pages dynamic
* **Bulk Conversion** - Convert all existing pages and posts with a single click
* **Admin Bar Actions** - Clear cache or regenerate all pages directly from the admin bar
* **Clean Uninstall** - Removes all cache files and settings when plugin is deleted
* **Logged-in User Bypass** - Administrators always see the dynamic version for editing
* **WooCommerce Compatible** - Automatically keeps cart, checkout, and product pages dynamic
* **Apache & Nginx Support** - Works with both major web servers
**How It Works:**
1. When a page or post is published, the plugin fetches the fully-rendered HTML
2. The HTML is saved to the cache directory, mirroring your site's URL structure
3. Future visitors receive the cached HTML file directly from the server
4. When content is updated, the cache is automatically regenerated
**Automatic Exclusions:**
The plugin intelligently excludes pages that require dynamic functionality:
* Pages with comments enabled
* Pages containing form shortcodes (Contact Form 7, WPForms, Gravity Forms, etc.)
* WooCommerce pages (cart, checkout, my account, products)
* Any page you manually mark as dynamic
== Installation ==
1. Upload the `wp-to-html` directory to `/wp-content/plugins/`
2. Activate the plugin through the 'Plugins' menu in WordPress
3. Go to Settings → WP to HTML to configure the plugin
4. Click "Convert All Pages & Posts" to generate static HTML for existing content
== Frequently Asked Questions ==
= Will this work with my theme? =
Yes! The plugin fetches the fully-rendered page including your theme, so it works with any WordPress theme.
= What about pages with dynamic content? =
Pages with dynamic content (forms, comments, WooCommerce features) are automatically excluded. You can also manually exclude any page using the checkbox in the editor.
= How do I know if a page is being served from cache? =
Cached pages include an HTML comment at the bottom with the generation timestamp. You can also check the response headers for `X-WP-To-HTML: served`.
= Does this work with Nginx? =
Yes, but you'll need to add the provided configuration rules to your Nginx server config. Apache servers work automatically via .htaccess.
= What happens if I deactivate the plugin? =
The cached files remain but are no longer served. Your site will function normally using dynamic PHP.
= What happens when I delete the plugin? =
When you delete the plugin through WordPress, all cache files and settings are automatically removed.
== Changelog ==
= 0.2.0 =
* Added scheduled regeneration via WordPress cron (hourly/twice daily/daily options)
* Added admin bar submenu with "Clear All Cache" and "Regenerate All" options
* Added uninstall.php to clean up cache files and settings on plugin deletion
* Added configurable cron interval in settings
= 0.1.2 =
* Added support for Custom Post Types
* Added settings link on plugin listing page
* Improved block exclusion detection
= 1.0.0 =
* Initial release
* Static HTML generation for pages and posts
* Automatic and manual exclusion options
* Bulk conversion tool
* WooCommerce compatibility
* Apache and Nginx support
+60
View File
@@ -0,0 +1,60 @@
<?php
/**
* Uninstall WP to HTML
*
* This file runs when the plugin is deleted from WordPress.
* It removes all plugin data including cached files and database options.
*/
// If uninstall not called from WordPress, exit
if (!defined('WP_UNINSTALL_PLUGIN')) {
exit;
}
// Define cache directory path
$cache_dir = WP_CONTENT_DIR . '/cache/wp-to-html/';
/**
* Recursively delete a directory and its contents
*
* @param string $dir Directory path
*/
function wp_to_html_recursive_delete($dir) {
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), array('.', '..'));
foreach ($files as $file) {
$path = $dir . '/' . $file;
if (is_dir($path)) {
wp_to_html_recursive_delete($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
// Delete cache directory and all contents
if (file_exists($cache_dir)) {
wp_to_html_recursive_delete($cache_dir);
}
// Delete plugin options
delete_option('wp_to_html_settings');
delete_option('wp_to_html_last_cron_run');
// Clear any scheduled cron events
$cron_hook = 'wp_to_html_scheduled_regeneration';
$timestamp = wp_next_scheduled($cron_hook);
if ($timestamp) {
wp_unschedule_event($timestamp, $cron_hook);
}
wp_clear_scheduled_hook($cron_hook);
// Clear the single regeneration events
wp_clear_scheduled_hook('wp_to_html_regenerate_post');
+271
View File
@@ -0,0 +1,271 @@
<?php
/**
* Plugin Name: WP to HTML
* Plugin URI: https://welldressedwalrus.com/
* Description: Converts WordPress pages and posts to static HTML files for faster loading. Supports exclusion rules for dynamic pages like forms.
* Version: 0.3.0
* Author: Jeffrey Long @ Well Dressed Walrus
* Author URI: https://welldressedwalrus.com
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
* Text Domain: wp-to-html
*/
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
// Plugin constants
define('WP_TO_HTML_VERSION', '0.3.0');
define('WP_TO_HTML_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WP_TO_HTML_PLUGIN_URL', plugin_dir_url(__FILE__));
define('WP_TO_HTML_CACHE_DIR', WP_CONTENT_DIR . '/cache/wp-to-html/');
define('WP_TO_HTML_CACHE_URL', content_url('/cache/wp-to-html/'));
/**
* Main plugin class
*/
class WP_To_HTML {
/**
* Single instance of the class
*/
private static $instance = null;
/**
* Get singleton instance
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct() {
$this->load_dependencies();
$this->init_hooks();
}
/**
* Load required class files
*/
private function load_dependencies() {
require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-generator.php';
require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-server.php';
require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-hooks.php';
require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-admin.php';
require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-bulk.php';
require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-cron.php';
require_once WP_TO_HTML_PLUGIN_DIR . 'includes/class-assets.php';
}
/**
* Initialize WordPress hooks
*/
private function init_hooks() {
// Initialize components
add_action('init', array($this, 'init_components'));
// Register activation/deactivation hooks
register_activation_hook(__FILE__, array($this, 'activate'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
}
/**
* Initialize plugin components
*/
public function init_components() {
WP_To_HTML_Generator::get_instance();
WP_To_HTML_Server::get_instance();
WP_To_HTML_Hooks::get_instance();
WP_To_HTML_Cron::get_instance();
if (is_admin()) {
WP_To_HTML_Admin::get_instance();
WP_To_HTML_Bulk::get_instance();
}
}
/**
* Plugin activation
*/
public function activate() {
// Create cache directory
if (!file_exists(WP_TO_HTML_CACHE_DIR)) {
wp_mkdir_p(WP_TO_HTML_CACHE_DIR);
}
// Create metadata directory
$metadata_dir = WP_TO_HTML_CACHE_DIR . '.metadata/';
if (!file_exists($metadata_dir)) {
wp_mkdir_p($metadata_dir);
}
// Add index.php to cache directory for security
$index_content = "<?php\n// Silence is golden.";
if (!file_exists(WP_TO_HTML_CACHE_DIR . 'index.php')) {
file_put_contents(WP_TO_HTML_CACHE_DIR . 'index.php', $index_content);
}
// Set default options
$default_options = array(
'enabled' => true,
'excluded_shortcodes' => 'contact-form-7,wpforms,gravityform,ninja_forms,formidable,wpcf7',
'excluded_blocks' => 'wpforms,gravityforms,contact-form-7,ninja-forms,formidable,forminator,ws-form,fluentform,everest-forms',
'bundle_css' => true,
'bundle_js' => true,
);
if (!get_option('wp_to_html_settings')) {
add_option('wp_to_html_settings', $default_options);
}
// Add htaccess rules
$this->add_htaccess_rules();
// Schedule cron event
WP_To_HTML_Cron::schedule_cron();
// Flush rewrite rules
flush_rewrite_rules();
}
/**
* Plugin deactivation
*/
public function deactivate() {
// Remove htaccess rules
$this->remove_htaccess_rules();
// Unschedule cron event
WP_To_HTML_Cron::unschedule_cron();
// Flush rewrite rules
flush_rewrite_rules();
// Note: We don't delete the cache directory on deactivation
// to preserve cached files in case of accidental deactivation
}
/**
* Add htaccess rules for serving static HTML
*/
private function add_htaccess_rules() {
$htaccess_file = ABSPATH . '.htaccess';
if (!is_writable($htaccess_file)) {
return false;
}
$htaccess_content = file_get_contents($htaccess_file);
// Check if rules already exist
if (strpos($htaccess_content, '# BEGIN WP-to-HTML') !== false) {
return true;
}
$rules = $this->get_htaccess_rules();
// Add rules before WordPress rules
$wp_marker = '# BEGIN WordPress';
if (strpos($htaccess_content, $wp_marker) !== false) {
$htaccess_content = str_replace($wp_marker, $rules . "\n\n" . $wp_marker, $htaccess_content);
} else {
$htaccess_content = $rules . "\n\n" . $htaccess_content;
}
return file_put_contents($htaccess_file, $htaccess_content);
}
/**
* Remove htaccess rules
*/
private function remove_htaccess_rules() {
$htaccess_file = ABSPATH . '.htaccess';
if (!is_writable($htaccess_file)) {
return false;
}
$htaccess_content = file_get_contents($htaccess_file);
// Remove our rules block
$pattern = '/# BEGIN WP-to-HTML.*?# END WP-to-HTML\s*/s';
$htaccess_content = preg_replace($pattern, '', $htaccess_content);
return file_put_contents($htaccess_file, $htaccess_content);
}
/**
* Get htaccess rules
*/
public function get_htaccess_rules() {
$cache_path = str_replace(ABSPATH, '/', WP_TO_HTML_CACHE_DIR);
$rules = "# BEGIN WP-to-HTML\n";
$rules .= "<IfModule mod_rewrite.c>\n";
$rules .= "RewriteEngine On\n";
$rules .= "RewriteBase /\n\n";
// Skip for logged-in users (WordPress login cookies)
$rules .= "# Skip for logged-in users\n";
$rules .= "RewriteCond %{HTTP_COOKIE} !wordpress_logged_in [NC]\n\n";
// Skip for POST requests
$rules .= "# Skip for POST requests\n";
$rules .= "RewriteCond %{REQUEST_METHOD} !POST\n\n";
// Skip for query strings
$rules .= "# Skip for requests with query strings\n";
$rules .= "RewriteCond %{QUERY_STRING} ^$\n\n";
// Check if static file exists and serve it
$rules .= "# Check if cached HTML exists and serve it\n";
$rules .= "RewriteCond %{DOCUMENT_ROOT}" . $cache_path . "%{REQUEST_URI}index.html -f\n";
$rules .= "RewriteRule ^(.*)$ " . $cache_path . "%{REQUEST_URI}index.html [L]\n";
$rules .= "</IfModule>\n";
$rules .= "# END WP-to-HTML";
return $rules;
}
/**
* Get plugin settings
*/
public static function get_settings() {
$defaults = array(
'enabled' => true,
'excluded_shortcodes' => 'contact-form-7,wpforms,gravityform,ninja_forms,formidable,wpcf7',
'excluded_blocks' => 'wpforms,gravityforms,contact-form-7,ninja-forms,formidable,forminator,ws-form,fluentform,everest-forms',
'cron_interval' => 'wp_to_html_hourly',
'bundle_css' => true,
'bundle_js' => true,
);
$settings = get_option('wp_to_html_settings', $defaults);
return wp_parse_args($settings, $defaults);
}
}
// Initialize the plugin
function wp_to_html_init() {
return WP_To_HTML::get_instance();
}
// Start the plugin
add_action('plugins_loaded', 'wp_to_html_init');
// Add settings link on plugin page
function wp_to_html_plugin_action_links($links) {
$settings_link = '<a href="' . admin_url('options-general.php?page=wp-to-html') . '">' . __('Settings', 'wp-to-html') . '</a>';
array_unshift($links, $settings_link);
return $links;
}
add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'wp_to_html_plugin_action_links');