From 1cf5bbeeb0a5a8804f557b003ea73da467926148 Mon Sep 17 00:00:00 2001
From: Jeffrey Long
Date: Wed, 18 Mar 2026 14:21:32 -0700
Subject: [PATCH] First Commit
---
admin/css/admin.css | 267 ++++++++++++
admin/js/admin.js | 378 +++++++++++++++++
includes/class-admin.php | 680 ++++++++++++++++++++++++++++++
includes/class-assets.php | 480 +++++++++++++++++++++
includes/class-bulk.php | 286 +++++++++++++
includes/class-cron.php | 270 ++++++++++++
includes/class-generator.php | 472 +++++++++++++++++++++
includes/class-hooks.php | 785 +++++++++++++++++++++++++++++++++++
includes/class-server.php | 138 ++++++
readme.txt | 98 +++++
uninstall.php | 60 +++
wp-to-html.php | 271 ++++++++++++
12 files changed, 4185 insertions(+)
create mode 100644 admin/css/admin.css
create mode 100644 admin/js/admin.js
create mode 100644 includes/class-admin.php
create mode 100644 includes/class-assets.php
create mode 100644 includes/class-bulk.php
create mode 100644 includes/class-cron.php
create mode 100644 includes/class-generator.php
create mode 100644 includes/class-hooks.php
create mode 100644 includes/class-server.php
create mode 100644 readme.txt
create mode 100644 uninstall.php
create mode 100644 wp-to-html.php
diff --git a/admin/css/admin.css b/admin/css/admin.css
new file mode 100644
index 0000000..98a72ee
--- /dev/null
+++ b/admin/css/admin.css
@@ -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);
+ }
+}
\ No newline at end of file
diff --git a/admin/js/admin.js b/admin/js/admin.js
new file mode 100644
index 0000000..019231a
--- /dev/null
+++ b/admin/js/admin.js
@@ -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 = '';
+ html += '
';
+ html += '
' + statusText + '';
+ if (sourceLabel) {
+ html += '
' + sourceLabel + '';
+ }
+ if (status.total && status.total > 0) {
+ html += '
' + status.processed + ' / ' + status.total + ' pages processed
';
+ }
+ html += '
';
+ } else if (status.status === 'complete') {
+ html = '';
+ html += '
';
+ html += '
' + strings.statusComplete + '';
+ html += '
' + status.converted + ' converted, ' + status.skipped + ' skipped, ' + status.errors + ' errors
';
+ html += '
';
+ } else {
+ // idle
+ html = '';
+ html += '';
+ html += '' + strings.statusIdle + '';
+ html += '
';
+ }
+
+ $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 = '';
+ html += '
' + this.results.converted + 'Converted
';
+ html += '
' + this.results.skipped + 'Skipped
';
+ html += '
' + this.results.errors + 'Errors
';
+ html += '
';
+
+ if (this.results.details.length > 0) {
+ html += '';
+
+ // 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 += '
';
+ html += '';
+ html += '' + this.escapeHtml(item.title) + '';
+ html += '' + this.escapeHtml(item.message) + '';
+ html += '
';
+ }
+
+ if (this.results.details.length > 50) {
+ html += '
Showing first 50 results...
';
+ }
+
+ html += '
';
+ }
+
+ $('#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);
+
diff --git a/includes/class-admin.php b/includes/class-admin.php
new file mode 100644
index 0000000..08c97e4
--- /dev/null
+++ b/includes/class-admin.php
@@ -0,0 +1,680 @@
+ 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');
+ }
+
+ ?>
+
+ ' . __('Configure the main settings for static HTML generation.', 'wp-to-html') . '
';
+ }
+
+ /**
+ * Render enabled field
+ */
+ public function render_enabled_field() {
+ $settings = WP_To_HTML::get_settings();
+ ?>
+
+
+
+
+ ' . __('Configure which pages should be excluded from static HTML generation.', 'wp-to-html') . '';
+ }
+
+ /**
+ * Render shortcodes field
+ */
+ public function render_shortcodes_field() {
+ $settings = WP_To_HTML::get_settings();
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ' . __('Configure automatic regeneration of stale cached pages.', 'wp-to-html') . '';
+ }
+
+ /**
+ * 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'),
+ );
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ()
+
+ ' . __('Configure performance optimizations for static HTML files.', 'wp-to-html') . '';
+ }
+
+ /**
+ * Render bundle CSS field
+ */
+ public function render_bundle_css_field() {
+ $settings = WP_To_HTML::get_settings();
+ ?>
+
+
+
+
+
+
+
+
+
+ get_cache_stats();
+
+ ?>
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+
+ |
+ |
+
+
+
+
+ |
+ |
+
+
+
+ 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);
+
+ ?>
+
+ 'idle',
+ ));
+ }
+
+ wp_send_json_success($status);
+ }
+}
diff --git a/includes/class-assets.php b/includes/class-assets.php
new file mode 100644
index 0000000..b473466
--- /dev/null
+++ b/includes/class-assets.php
@@ -0,0 +1,480 @@
+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
+ $bundled_link = '';
+ $html = str_replace('', $bundled_link . "\n", $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