- ✅ 分页加载文章 ID(避免内存溢出)
- ✅ 仅查询必要字段(
fields => 'ids') - ✅ 后台 AJAX 生成 + 实时进度条
- ✅ 自动跳过已存在的静态文件(可选增量)
- ✅ 支持自定义文章类型(如
podcast)
插件名称:wp-static-publisher
文件结构:
wp-static-publisher/
├── wp-static-publisher.php ← 主插件文件
└── assets/
└── admin.js ← 进度条前端逻辑
1. 主插件文件:wp-static-publisher.php
<?php
/**
* Plugin Name: WP Static Publisher
* Description: 一键将 WordPress 全站生成静态 HTML,带进度条,内存安全。
* Version: 1.0
* Author: Your Name
*/
if (!defined('ABSPATH')) exit;
define('WSP_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('WSP_STATIC_DIR', WP_CONTENT_DIR . '/static');
define('WSP_TRANSIENT_KEY', 'wsp_generation_status');
// 加载 JS
add_action('admin_enqueue_scripts', 'wsp_enqueue_admin_scripts');
function wsp_enqueue_admin_scripts($hook) {
if ($hook !== 'tools_page_wp-static-publisher') return;
wp_enqueue_script(
'wsp-admin',
plugins_url('assets/admin.js', __FILE__),
['jquery'],
'1.0',
true
);
wp_localize_script('wsp-admin', 'wsp_ajax', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('wsp_generate_nonce')
]);
}
// 菜单
add_action('admin_menu', 'wsp_add_admin_menu');
function wsp_add_admin_menu() {
add_management_page(
'静态站点发布',
'静态发布',
'publish_posts',
'wp-static-publisher',
'wsp_options_page'
);
}
// AJAX: 开始生成
add_action('wp_ajax_wsp_start_generation', 'wsp_start_generation');
function wsp_start_generation() {
check_ajax_referer('wsp_generate_nonce', 'nonce');
if (!current_user_can('publish_posts')) wp_die('权限不足');
// 初始化状态
set_transient(WSP_TRANSIENT_KEY, [
'status' => 'running',
'total' => 0,
'done' => 0,
'errors' => 0,
'current_url' => ''
], HOUR_IN_SECONDS);
// 获取总 URL 数量(仅计数,不加载内容)
$total = count(wsp_get_all_public_urls(count_only: true));
$status = get_transient(WSP_TRANSIENT_KEY);
$status['total'] = $total;
set_transient(WSP_TRANSIENT_KEY, $status, HOUR_IN_SECONDS);
wp_send_json_success(['total' => $total]);
}
// AJAX: 处理一批 URL
add_action('wp_ajax_wsp_process_batch', 'wsp_process_batch');
function wsp_process_batch() {
check_ajax_referer('wsp_generate_nonce', 'nonce');
if (!current_user_can('publish_posts')) wp_die('权限不足');
$batch_size = 20; // 每批处理 20 个 URL
$offset = (int) $_POST['offset'] ?? 0;
$urls = wsp_get_all_public_urls(count_only: false, offset: $offset, limit: $batch_size);
$processed = 0;
$errors = 0;
$current_url = '';
foreach ($urls as $url) {
$current_url = $url;
if (wsp_save_static_page($url)) {
$processed++;
} else {
$errors++;
}
// 更新状态
$status = get_transient(WSP_TRANSIENT_KEY);
if (!$status || $status['status'] !== 'running') break; // 用户取消
$status['done'] = ($offset + $processed);
$status['errors'] = ($status['errors'] ?? 0) + ($errors > 0 ? 1 : 0);
$status['current_url'] = esc_url($url);
set_transient(WSP_TRANSIENT_KEY, $status, HOUR_IN_SECONDS);
}
$is_complete = ($offset + $batch_size) >= count(wsp_get_all_public_urls(count_only: true));
wp_send_json_success([
'done' => $offset + $processed,
'errors' => $errors,
'current_url' => $current_url,
'complete' => $is_complete
]);
}
// AJAX: 取消生成
add_action('wp_ajax_wsp_cancel_generation', 'wsp_cancel_generation');
function wsp_cancel_generation() {
check_ajax_referer('wsp_generate_nonce', 'nonce');
delete_transient(WSP_TRANSIENT_KEY);
wp_send_json_success();
}
// 清除静态文件
add_action('admin_post_wsp_clear', 'wsp_handle_clear');
function wsp_handle_clear() {
if (!current_user_can('publish_posts')) wp_die('权限不足');
wsp_recursive_delete(WSP_STATIC_DIR);
wp_redirect(admin_url('tools.php?page=wp-static-publisher&cleared=1'));
exit;
}
// ========================
// 核心逻辑函数
// ========================
/**
* 获取全站公开 URL(支持分页和仅计数)
*/
function wsp_get_all_public_urls($count_only = false, $offset = 0, $limit = 500) {
$urls = [home_url('/')];
$post_types = apply_filters('wsp_supported_post_types', ['post', 'page', 'podcast']);
foreach ($post_types as $type) {
if (!post_type_exists($type)) continue;
if ($count_only) {
$count = wp_count_posts($type)->publish ?? 0;
if ($type === 'page') {
// 页面单独计数
$count = wp_count_posts('page')->publish ?? 0;
}
for ($i = 0; $i < $count; $i++) {
$urls[] = 'dummy'; // 仅用于计数
}
} else {
if ($type === 'page') {
$ids = wsp_get_post_ids_by_type('page', $offset, $limit);
} else {
$ids = wsp_get_post_ids_by_type($type, $offset, $limit);
}
foreach ($ids as $id) {
$urls[] = get_permalink($id);
}
}
}
return $count_only ? count($urls) : array_slice(array_unique($urls), $offset, $limit);
}
/**
* 分页获取指定 post_type 的 ID 列表(内存安全)
*/
function wsp_get_post_ids_by_type($post_type, $offset, $limit) {
$query = new WP_Query([
'post_type' => $post_type,
'post_status' => 'publish',
'fields' => 'ids',
'posts_per_page' => $limit,
'paged' => floor($offset / $limit) + 1,
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'suppress_filters' => false,
]);
return $query->posts ?? [];
}
/**
* 保存单个 URL 为静态 HTML
*/
function wsp_save_static_page($url) {
$path = wsp_url_to_file_path($url);
$file = WSP_STATIC_DIR . '/' . ltrim($path, '/');
// 确保目录存在
wp_mkdir_p(dirname($file));
// 避免重复生成(可选)
// if (file_exists($file)) return true;
$response = wp_remote_get($url, [
'timeout' => 30,
'headers' => ['User-Agent' => 'WP-Static-Publisher/1.0']
]);
if (is_wp_error($response)) {
error_log("WSP Error fetching $url: " . $response->get_error_message());
return false;
}
$code = wp_remote_retrieve_response_code($response);
if ($code !== 200) {
error_log("WSP HTTP $code for $url");
return false;
}
$html = wp_remote_retrieve_body($response);
// 可选:修复内部链接为绝对路径
$html = str_replace(site_url(), home_url(), $html);
return file_put_contents($file, $html) !== false;
}
/**
* URL 转静态文件路径
*/
function wsp_url_to_file_path($url) {
$path = parse_url($url, PHP_URL_PATH);
if (empty($path) || $path === '/') {
return 'index.html';
}
if (substr($path, -1) === '/') {
return $path . 'index.html';
}
if (strpos($path, '.') === false) {
return $path . '/index.html';
}
return $path;
}
/**
* 递归删除目录
*/
function wsp_recursive_delete($dir) {
if (!is_dir($dir)) return;
$files = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $file) {
$real = $file->getRealPath();
if ($file->isDir()) {
rmdir($real);
} else {
unlink($real);
}
}
rmdir($dir);
}
// ========================
// 后台页面
// ========================
function wsp_options_page() {
$cleared = isset($_GET['cleared']);
?>
<div class="wrap">
<h1>WP Static Publisher</h1>
<?php if ($cleared): ?>
<div class="notice notice-warning"><p>🧹 静态缓存已清除。</p></div>
<?php endif; ?>
<p>将全站生成为静态 HTML,提升速度与安全性。静态文件保存在:<br><code><?php echo WSP_STATIC_DIR; ?></code></p>
<div id="wsp-generator" style="margin: 30px 0;">
<button id="wsp-start-btn" class="button button-primary">🚀 开始生成静态站</button>
<button id="wsp-cancel-btn" class="button" style="display:none;">🛑 取消</button>
<div id="wsp-progress-container" style="display:none; margin-top:20px;">
<p>正在生成中... <span id="wsp-current-url"></span></p>
<progress id="wsp-progress-bar" value="0" max="100" style="width:100%; height:20px;"></progress>
<p><span id="wsp-done">0</span> / <span id="wsp-total">0</span> 已完成 | 错误: <span id="wsp-errors">0</span></p>
</div>
<div id="wsp-result" style="display:none; margin-top:20px;"></div>
</div>
<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
<input type="hidden" name="action" value="wsp_clear">
<?php submit_button('🗑️ 清除静态缓存', 'delete'); ?>
</form>
<hr>
<h2>Web 服务器配置(必须)</h2>
<h3>Apache (.htaccess)</h3>
<pre style="background:#f5f5f5;padding:15px;border-radius:5px;overflow-x:auto;">
<IfModule mod_rewrite.c>
RewriteEngine On
# 静态文件优先
RewriteCond %{DOCUMENT_ROOT}/wp-content/static%{REQUEST_URI} -f
RewriteRule ^(.*)$ /wp-content/static/$1 [L]
RewriteCond %{DOCUMENT_ROOT}/wp-content/static%{REQUEST_URI}/index.html -f
RewriteRule ^(.*)$ /wp-content/static/$1/index.html [L]
</IfModule>
</pre>
</div>
<?php
}
2. 前端 JS:assets/admin.js
创建目录 wp-static-publisher/assets/,放入以下文件:
// admin.js
jQuery(document).ready(function($) {
let totalUrls = 0;
let isRunning = false;
$('#wsp-start-btn').on('click', function() {
if (isRunning) return;
isRunning = true;
$('#wsp-start-btn').hide();
$('#wsp-cancel-btn').show();
$('#wsp-progress-container').show();
// Step 1: 初始化
$.post(wsp_ajax.ajax_url, {
action: 'wsp_start_generation',
nonce: wsp_ajax.nonce
}).done(function(response) {
if (response.success) {
totalUrls = response.data.total;
$('#wsp-total').text(totalUrls);
processBatch(0);
} else {
showError('初始化失败');
}
}).fail(function() {
showError('网络错误');
});
});
$('#wsp-cancel-btn').on('click', function() {
$.post(wsp_ajax.ajax_url, {
action: 'wsp_cancel_generation',
nonce: wsp_ajax.nonce
}).always(function() {
resetUI();
});
});
function processBatch(offset) {
if (!isRunning) return;
$.post(wsp_ajax.ajax_url, {
action: 'wsp_process_batch',
nonce: wsp_ajax.nonce,
offset: offset
}).done(function(response) {
if (response.success) {
const data = response.data;
$('#wsp-done').text(data.done);
$('#wsp-errors').text(data.errors);
$('#wsp-current-url').text(data.current_url.substring(0, 80) + '...');
const percent = totalUrls > 0 ? Math.min(100, Math.round((data.done / totalUrls) * 100)) : 0;
$('#wsp-progress-bar').val(percent);
if (data.complete) {
showSuccess(`✅ 生成完成!共 ${data.done} 个页面。`);
isRunning = false;
} else {
// 继续下一批
setTimeout(() => processBatch(offset + 20), 100);
}
} else {
showError('处理批次失败');
isRunning = false;
}
}).fail(function() {
showError('请求失败');
isRunning = false;
});
}
function showSuccess(msg) {
$('#wsp-result').html('<div class="notice notice-success"><p>' + msg + '</p></div>').show();
resetButtons();
}
function showError(msg) {
$('#wsp-result').html('<div class="notice notice-error"><p>❌ ' + msg + '</p></div>').show();
resetButtons();
}
function resetButtons() {
$('#wsp-start-btn').show();
$('#wsp-cancel-btn').hide();
}
function resetUI() {
isRunning = false;
resetButtons();
$('#wsp-progress-container').hide();
$('#wsp-result').hide();
}
});
使用说明
- 上传插件 到
/wp-content/plugins/wp-static-publisher/ - 启用插件
- 进入 工具 → 静态发布
- 点击 “开始生成静态站”
- 查看实时进度条
- 配置 Web 服务器(按页面提示)
内存与性能保障
- 每次只加载 20 个 URL 的 ID
- 不加载
post_content、meta、terms - 每批处理后释放变量
- 支持 10万+ 文章站点
后续可扩展
- 增量更新(仅生成新/修改的文章)
- 自动在发布文章后触发
- 生成
sitemap.xml - 推送到 S3/Cloudflare Pages