WordPress 來開發一個可以從後台上傳範本檔案的管理外掛

先聊聊這個外掛的需求面,在工作中遇到一些特殊的使用方法,在 WP 的架構下,想要在 WP 產生一個網址顯示一頁面,需要發布一篇文章或頁面,但又希望這個頁面跟原本 WP 的主題的 Html 結構與 CSS、js 毫無關連,要一個獨立的頁面,而需要頻繁的增加頁面,頁面的 Html 結構與 CSS 都不一樣,CSS、JS、圖片都使用外聯方式,不會用到主題 heard.php、fooder.php 完全獨立頁面,講到這是不是有點怪,或許有些企業也是這樣使用 WP 🤣目的只是要使用 WP 架構下所產生出來的網址。

既然要開發一個管理範本上傳檔案功能的外掛,範本檔案(php)、CSS、JS、圖片將會回到 WP 的架構中,規劃一個上傳範本檔案(php)頁面包含範本檔案列表顯示功能,可以處裡刪除範檔案功能,範本檔案上傳後就可以在頁面編輯功能中的範本選項中選取上傳的範本,這樣就可以在後台管理,不用透過FTP上傳,雖然已有像 Element 不需要程式碼來自定頁面版型外掛,何必再搞一個上傳檔案,是沒錯!但版面可自定彈性沒有純 Code 大就是了,不懂 code 確實很方便,就看需求。

整理一下功能

  • 可上傳範本檔案(php),能在頁面編輯中選擇範本。
  • 有範本檔案列表進行刪除管理。
  • 可以上傳自定CSS、JS 檔案,至於是否要開個列表來管理再看看。
  • 上傳檔案都在此外掛的資料夾內,以不干擾主題檔案。

差不多就這些功能,有些功能可能就先不完成,感覺這篇文章會太長會寫不完,先來完成上傳檔案與選擇範本功能,編輯檔案功能就用 WP 內建可以省時間,那就開始~!

自製外掛起手式

*一句老話,程式碼要放在佈景主題的 function 裡,還是要放在外掛裡,來使用這功能都可,外掛起手式這裡不再詳述,去看WordPress custom post type 如何自訂文章類型這篇😃

在本機建立伺服器環境

*要在本機建立 PHP、阿帕契網站環境可以看這篇如何使用 Python 爬取原價屋價目列表😜,別辛苦在遠端測試所寫的程式。

建立一個自定外掛

在 WP plugin 資料夾建立一個資料夾為 add-template-manage,建立一個 add-template-manage.php 檔案在裡面添加程式碼如下:

PHP
<?php
/**
 * @package Add_Tamplate_Management
 * @version 1.0.0
 */
/*
Plugin Name: 增加範本管理
Plugin URI: https://github.com/nonelin/add-template-management
Description: 從後臺上傳範本檔案,方便在頁面中選擇範本,這裡只會管理外掛所上船的範本檔案。
Author: AWei
Version: 1.0.0
Author URI: https://dafatime.idv.tw/
*/

上面檔案儲存後,在外掛頁面就可以看到,只要啟動就可以運作外掛,但目前沒有寫入任何功能所以看不出有啥變化。

建立自定後台頁面

PHP
// 增加自訂選單
function add_tamplate_files_menu() {
    // 使用WordPress提供的add_menu_page()函數創建一個新的管理選單
    add_menu_page(
      '增加範本',   // 選單主標題
      '增加範本',   // 選單名稱
      'manage_options', // 許可權
      'add_tamplate_files', // 選單ID 必須唯一
      'add_tamplate_files_html', // 要放入的內容函數
      'dashicons-admin-generic', // 選單圖標
      99 // 選單位置
    );
  }

  // 將custom_menu函數增加到WordPress的admin_menu鉤子
add_action('admin_menu', 'add_tamplate_files_menu');

// 自訂選單要內頁放入的內容
function add_tamplate_files_html() {
  ?>
<div class="wrap">
<h1>上傳範本檔案</h1>
<form method="post" enctype="multipart/form-data">
  <input type="file" name="template_file" required />
  <input type="submit" class="button button-primary" value="上傳" />
</form>
</div>
<?php
}

在 Admin 後台選單添加一個上傳頁面,下面在添加上傳檔案規則。

增加上傳功能

PHP
// 增加自訂選單
function add_tamplate_files_menu() {
    // 使用WordPress提供的add_menu_page()函數創建一個新的管理選單
    add_menu_page(
      '增加範本',   // 選單主標題
      '增加範本',   // 選單名稱
      'manage_options', // 許可權
      'add_tamplate_files', // 選單ID 必須唯一
      'add_tamplate_files_html', // 要放入的內容函數
      'dashicons-admin-generic', // 選單圖標
      99 // 選單位置
    );
  }

  // 將custom_menu函數增加到WordPress的admin_menu鉤子
add_action('admin_menu', 'add_tamplate_files_menu');

// 自訂選單要內頁放入的內容
function add_tamplate_files_html() {
  // 檢查是否有檔案上傳請求
  if (isset($_FILES['template_file']) && current_user_can('manage_options') && check_admin_referer('upload_template_file', 'template_file_nonce')) {
    $upload_dir = plugin_dir_path(__FILE__) . 'templates/';
    if (!file_exists($upload_dir)) {
      mkdir($upload_dir, 0755, true);
    }
    $file = $_FILES['template_file'];
    $filename = basename($file['name']);
    $target = $upload_dir . $filename;
    $allowed = array('php');
    $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
    if (in_array($ext, $allowed)) {
      if (move_uploaded_file($file['tmp_name'], $target)) {
        echo '<div class="updated"><p>檔案上傳成功!</p></div>';
      } else {
        echo '<div class="error"><p>檔案上傳失敗。</p></div>';
      }
    } else {
      echo '<div class="error"><p>只允許上傳 php 檔案。</p></div>';
    }
  }
?>
<div class="wrap">
<h1>上傳範本檔案</h1>
<form method="post" enctype="multipart/form-data">
  <?php wp_nonce_field('upload_template_file', 'template_file_nonce'); ?>
  <input type="file" name="template_file" required />
  <input type="submit" class="button button-primary" value="上傳" />
</form>
</div>
<?php
}

到這裡新增的上傳頁面就可以把範本檔案上傳到外掛內的 templates 資料夾中,有看到上傳的檔案就表示這個功能成功,再來將上傳的檔案使用 hook 讓頁面編輯可以選擇到上傳的範本。

hook 外掛中的範本

PHP
// 將外掛 templates 資料夾中的檔案動態加入頁面範本選單
add_filter('theme_page_templates', function($templates) {
    $plugin_templates_dir = plugin_dir_path(__FILE__) . 'templates/';
    if (file_exists($plugin_templates_dir)) {
        $files = glob($plugin_templates_dir . '*.php');
        foreach ($files as $file) {
            $filename = basename($file);
            // 範本名稱取至範本檔案內的Template Name註解名稱
            $file_contents = file_get_contents($file);
            if (preg_match('/Template Name:\\s*(.+)/i', $file_contents, $matches)) {
                $template_name = trim($matches[1]);
            } else {
                $template_name = preg_replace('/\\.php$/', '', $filename);
            }
            $templates['add-template/' . $filename] = '[P] ' . $template_name;
        }
    }
    return $templates;
});

// 讓 WordPress 能正確載入外掛範本
add_filter('template_include', function($template) {
    if (is_page()) {
        $selected = get_page_template_slug(get_queried_object_id());
        if (strpos($selected, 'add-template/') === 0) {
            $plugin_template = plugin_dir_path(__FILE__) . 'templates/' . basename($selected);
            if (file_exists($plugin_template)) {
                return $plugin_template;
            }
        }
    }
    return $template;
});

由於後面範本會不斷增加或減少,這裡有兩個 hook 一個是動態添加範本,另一個是將外掛的範本正確載入,透過這兩個 hook 就可以在頁面編輯時在範本選擇看到上傳的範本。

添加上傳範本列表

PHP
// 取得範本資料夾中的所有檔案
$template_dir = plugin_dir_path(__FILE__) . 'templates/';
$template_files = glob($template_dir . '*.*');

// 處理檔案刪除請求
if(isset($_POST['delete_template']) && current_user_can('manage_options')) {
  $file_to_delete = sanitize_text_field($_POST['delete_template']);
  $file_path = $template_dir . basename($file_to_delete);
  
  if(file_exists($file_path) && unlink($file_path)) {
    echo '<div class="updated"><p>檔案已成功刪除!</p></div>';
  } else {
    echo '<div class="error"><p>刪除檔案時發生錯誤。</p></div>';
  }
}
?>

<h2>已上傳的範本檔案</h2>
<?php 
// 重新取得範本檔案列表,這裡不加刪除檔案不會刷新列表。
$template_files = glob($template_dir . '*.*');

if(!empty($template_files)): ?>
  <table class="wp-list-table widefat fixed striped">
    <thead>
      <tr>
        <th>檔案名稱</th>
        <th>範本名稱</th>
        <th>時間</th>
        <th>操作</th>
      </tr>
    </thead>
    <tbody>
      <?php foreach($template_files as $file): 
        $filename = basename($file);
        $file_time = filemtime($file);
        $file_contents = file_exists($file) ? file_get_contents($file) : '';
        
        // 取得範本名稱
        if(preg_match('/Template Name:\\s*(.+)/i', $file_contents, $matches)) {
          $template_name = trim($matches[1]);
        } else {
          $template_name = preg_replace('/\\.php$/', '', $filename);
        }
      ?>
        <tr>
          <td><?php echo esc_html($filename); ?></td>
          <td><?php echo esc_html($template_name); ?></td>
          <td><?php echo date('Y-m-d H:i:s', $file_time); ?></td>
          <td>
            <form method="post" style="display:inline;">
              <input type="hidden" name="delete_template" value="<?php echo esc_attr($filename); ?>">
              <button type="submit" class="button button-small" onclick="return confirm('確定要刪除此範本檔案嗎?');">刪除</button>
            </form>
          </td>
        </tr>
      <?php endforeach; ?>
    </tbody>
  </table>
<?php else: ?>
  <p>目前沒有已上傳的範本檔案。</p>
<?php endif; ?>

上面程式碼可以增加範本頁面看到,下面就是上傳的範本檔案列表,按下刪除按鈕就會將檔案刪除,現在基本上核心功能都完成了!

製作範本檔案

製作一個簡單的範本,上面有提到 CSS、JS 圖片要放那,這邊我就先使用 Bootstrap 框架外連 CDN 的 CSS 吧!圖片就直接連沒題庫的圖片。

PHP
<?php
/*
Template Name: 第二個自訂範本
*/
?>

<!DOCTYPE html>
<html data-bs-theme="light" lang="en">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
    <title>test-thme</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-4Q6Gf2aSP4eDXB8Miphtr37CMZZQ5oXLH2yaXMJ2w8e2ZtHTl7GptT4jmndRuHDT" crossorigin="anonymous">
</head>

<body>
    <nav class="navbar navbar-expand-md bg-body py-3">
        <div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><span class="bs-icon-sm bs-icon-rounded bs-icon-primary d-flex justify-content-center align-items-center me-2 bs-icon"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-bezier">
                        <path fill-rule="evenodd" d="M0 10.5A1.5 1.5 0 0 1 1.5 9h1A1.5 1.5 0 0 1 4 10.5v1A1.5 1.5 0 0 1 2.5 13h-1A1.5 1.5 0 0 1 0 11.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zm10.5.5A1.5 1.5 0 0 1 13.5 9h1a1.5 1.5 0 0 1 1.5 1.5v1a1.5 1.5 0 0 1-1.5 1.5h-1a1.5 1.5 0 0 1-1.5-1.5zm1.5-.5a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5zM6 4.5A1.5 1.5 0 0 1 7.5 3h1A1.5 1.5 0 0 1 10 4.5v1A1.5 1.5 0 0 1 8.5 7h-1A1.5 1.5 0 0 1 6 5.5zM7.5 4a.5.5 0 0 0-.5.5v1a.5.5 0 0 0 .5.5h1a.5.5 0 0 0 .5-.5v-1a.5.5 0 0 0-.5-.5z"></path>
                        <path d="M6 4.5H1.866a1 1 0 1 0 0 1h2.668A6.517 6.517 0 0 0 1.814 9H2.5c.123 0 .244.015.358.043a5.517 5.517 0 0 1 3.185-3.185A1.503 1.503 0 0 1 6 5.5zm3.957 1.358A1.5 1.5 0 0 0 10 5.5v-1h4.134a1 1 0 1 1 0 1h-2.668a6.517 6.517 0 0 1 2.72 3.5H13.5c-.123 0-.243.015-.358.043a5.517 5.517 0 0 0-3.185-3.185z"></path>
                    </svg></span><span>Brand</span></a><button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-1"><span class="visually-hidden">Toggle navigation</span><span class="navbar-toggler-icon"></span></button>
            <div class="collapse navbar-collapse" id="navcol-1">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item"><a class="nav-link active" href="#">First Item</a></li>
                    <li class="nav-item"><a class="nav-link" href="#">Second Item</a></li>
                    <li class="nav-item"><a class="nav-link" href="#">Third Item</a></li>
                </ul><button class="btn btn-primary" type="button">Button</button>
            </div>
        </div>
    </nav>
    <section class="py-4 py-xl-5">
        <div class="container h-100">
            <div class="row h-100">
                <div class="col-md-10 col-xl-8 text-center d-flex d-sm-flex d-md-flex justify-content-center align-items-center justify-content-md-start align-items-md-center justify-content-xl-center mx-auto">
                    <div>
                        <h2 class="text-uppercase fw-bold mb-3">打發時間<br>www.dafatime.idv.tw</h2>
                        <p class="mb-4">Etiam a rutrum, mauris lectus aptent convallis. Fusce vulputate aliquam, sagittis odio metus. Nulla porttitor vivamus viverra laoreet, aliquam netus.</p><button class="btn btn-primary fs-5 me-2 px-4 py-2" type="button">Button</button><button class="btn btn-outline-primary fs-5 px-4 py-2" type="button">Button</button>
                    </div>
                </div>
            </div>
        </div>
    </section>
    <div class="container py-4 py-xl-5">
        <div class="row mb-5">
            <div class="col-md-8 col-xl-6 text-center mx-auto">
                <h2>Heading</h2>
                <p>Curae hendrerit donec commodo hendrerit egestas tempus, turpis facilisis nostra nunc. Vestibulum dui eget ultrices.</p>
            </div>
        </div>
        <div class="row gy-4 row-cols-1 row-cols-md-2 row-cols-xl-3">
            <div class="col">
                <div><img class="rounded img-fluid object-fit-cover d-block w-100" style="height: 200px;" src="https://dafatime.idv.tw/wp-content/uploads/2024/01/l10crud00.png">
                    <div class="py-4">
                        <h4>Lorem libero donec</h4>
                        <p>Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
                    </div>
                </div>
            </div>
            <div class="col">
                <div><img class="rounded img-fluid object-fit-cover d-block w-100" style="height: 200px;" src="https://dafatime.idv.tw/wp-content/uploads/2025/03/livewire-pagination00.png">
                    <div class="py-4">
                        <h4>Lorem libero donec</h4>
                        <p>Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
                    </div>
                </div>
            </div>
            <div class="col">
                <div><img class="rounded img-fluid object-fit-cover d-block w-100" style="height: 200px;" src="https://dafatime.idv.tw/wp-content/uploads/2025/04/wp_slow_00.png">
                    <div class="py-4">
                        <h4>Lorem libero donec</h4>
                        <p>Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
                    </div>
                </div>
            </div>
            <div class="col">
                <div><img class="rounded img-fluid object-fit-cover d-block w-100" style="height: 200px;" src="https://dafatime.idv.tw/wp-content/uploads/2024/12/LaravelandLivewire3.png">
                    <div class="py-4">
                        <h4>Lorem libero donec</h4>
                        <p>Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec id elit non mi porta gravida at eget metus.</p>
                    </div>
                </div>
            </div>
        </div>
        <footer class="text-center bg-body" data-bs-theme="light">
            <div class="container py-4 py-lg-5">
                <ul class="list-inline">
                    <li class="list-inline-item me-4"><a class="link-body-emphasis" href="#">Web design</a></li>
                    <li class="list-inline-item me-4"><a class="link-body-emphasis" href="#">Development</a></li>
                    <li class="list-inline-item"><a class="link-body-emphasis" href="#">Hosting</a></li>
                </ul>
                <ul class="list-inline">
                    <li class="list-inline-item me-4"><a href="#"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-facebook text-body">
                                <path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951"></path>
                            </svg></a></li>
                    <li class="list-inline-item me-4"><a href="#"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-twitter-x text-body">
                                <path d="M12.6.75h2.454l-5.36 6.142L16 15.25h-4.937l-3.867-5.07-4.425 5.07H.316l5.733-6.57L0 .75h5.063l3.495 4.633L12.601.75Zm-.86 13.028h1.36L4.323 2.145H2.865l8.875 11.633Z"></path>
                            </svg></a></li>
                    <li class="list-inline-item"><a href="#"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 16 16" class="bi bi-instagram text-body">
                                <path d="M8 0C5.829 0 5.556.01 4.703.048 3.85.088 3.269.222 2.76.42a3.917 3.917 0 0 0-1.417.923A3.927 3.927 0 0 0 .42 2.76C.222 3.268.087 3.85.048 4.7.01 5.555 0 5.827 0 8.001c0 2.172.01 2.444.048 3.297.04.852.174 1.433.372 1.942.205.526.478.972.923 1.417.444.445.89.719 1.416.923.51.198 1.09.333 1.942.372C5.555 15.99 5.827 16 8 16s2.444-.01 3.298-.048c.851-.04 1.434-.174 1.943-.372a3.916 3.916 0 0 0 1.416-.923c.445-.445.718-.891.923-1.417.197-.509.332-1.09.372-1.942C15.99 10.445 16 10.173 16 8s-.01-2.445-.048-3.299c-.04-.851-.175-1.433-.372-1.941a3.926 3.926 0 0 0-.923-1.417A3.911 3.911 0 0 0 13.24.42c-.51-.198-1.092-.333-1.943-.372C10.443.01 10.172 0 7.998 0h.003zm-.717 1.442h.718c2.136 0 2.389.007 3.232.046.78.035 1.204.166 1.486.275.373.145.64.319.92.599.28.28.453.546.598.92.11.281.24.705.275 1.485.039.843.047 1.096.047 3.231s-.008 2.389-.047 3.232c-.035.78-.166 1.203-.275 1.485a2.47 2.47 0 0 1-.599.919c-.28.28-.546.453-.92.598-.28.11-.704.24-1.485.276-.843.038-1.096.047-3.232.047s-2.39-.009-3.233-.047c-.78-.036-1.203-.166-1.485-.276a2.478 2.478 0 0 1-.92-.598 2.48 2.48 0 0 1-.6-.92c-.109-.281-.24-.705-.275-1.485-.038-.843-.046-1.096-.046-3.233 0-2.136.008-2.388.046-3.231.036-.78.166-1.204.276-1.486.145-.373.319-.64.599-.92.28-.28.546-.453.92-.598.282-.11.705-.24 1.485-.276.738-.034 1.024-.044 2.515-.045v.002zm4.988 1.328a.96.96 0 1 0 0 1.92.96.96 0 0 0 0-1.92zm-4.27 1.122a4.109 4.109 0 1 0 0 8.217 4.109 4.109 0 0 0 0-8.217zm0 1.441a2.667 2.667 0 1 1 0 5.334 2.667 2.667 0 0 1 0-5.334"></path>
                            </svg></a></li>
                </ul>
                <p class="text-body mb-0">Copyright © 2025 Brand</p>
            </div>
        </footer>
    </div>
</body>

</html>

最後

這支外掛的基本功能就這樣,可以正常運作,至於要怎麼使用這個功能的外掛就看需求,我會將這支外掛放上 Github 大家可以自行安裝到 WP 中使用,上面程式碼中應該有發現 Github 連結了吧!外掛內的 CSS、JS 資料夾先保留,後續會用到,再看看怎麼用,這篇有功能性的外掛開發就這樣啦!覺得外掛符合需求就好好享受,掰掰。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *