First Commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
));
|
||||
});
|
||||
@@ -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
@@ -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
|
||||
@@ -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
@@ -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');
|
||||
Reference in New Issue
Block a user