wordpress全站静态化方案

  • 分页加载文章 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;">
&lt;IfModule mod_rewrite.c&gt;
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]
&lt;/IfModule&gt;
</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();
    }
});

使用说明

  1. 上传插件/wp-content/plugins/wp-static-publisher/
  2. 启用插件
  3. 进入 工具 → 静态发布
  4. 点击 “开始生成静态站”
  5. 查看实时进度条
  6. 配置 Web 服务器(按页面提示)

内存与性能保障

  • 每次只加载 20 个 URL 的 ID
  • 不加载 post_content、meta、terms
  • 每批处理后释放变量
  • 支持 10万+ 文章站点

后续可扩展

  • 增量更新(仅生成新/修改的文章)
  • 自动在发布文章后触发
  • 生成 sitemap.xml
  • 推送到 S3/Cloudflare Pages

Comments

No comments yet. Why don’t you start the discussion?

发表回复