如果把網站的內容庫想成一間倉庫,WordPress 內建的「文章」就是最大的那一區,什麼東西都往那裡塞,靠分類與標籤在架上貼便條紙做區分。可是有些內容天生就不適合塞進這一區,它需要自己的欄位、自己的網址規則、自己的清單頁,就像超市把生鮮跟乾貨分開陳列,不是在同一區貼不同顏色標籤就能解決。自訂文章類型(Custom Post Type,簡稱 CPT),做的正是這件事,幫某一種內容單獨開一個專屬櫃位,而不是繼續塞進文章這個大倉庫再靠標籤區分。
常見的狀況是,網站做到一半發現要放「作品集」「服務項目」或「常見問答」,第一個念頭往往是多開幾個分類就好,於是把這些內容全部發成文章、掛上對應分類。結果沒多久就會碰壁,作品集想要自己的網址結構,也想要獨立的欄位,例如案主名稱、完成年份,還不想跟部落格文章擠在同一份清單裡管理,但分類做不到這幾件事,它只能替同一種內容貼標籤,管不了欄位、網址跟清單頁這種結構性的差異。
自訂文章類型就是讓開發者自己向 WordPress 註冊一種新的內容類型,跟文章、頁面平起平坐,各自有各自的欄位組合、網址前綴、後台清單。搞懂它跟分類、標籤的分工界線,之後遇到「這批內容該怎麼放」的判斷題就不會拿不定主意,也不會把不該混在一起的東西硬塞進同一個籃子。先從自訂文章類型跟分類、標籤的界線講起,再一路拆到 register_post_type() 每個參數怎麼設、常見會出錯的地方在哪裡。
自訂文章類型是什麼?和分類、標籤的分工界線
WordPress 資料庫其實只有一張表在存所有內容,wp_posts,不管是一般文章、靜態頁面還是媒體檔案,全部擠在同一張表裡,靠一個叫 post_type 的欄位分辨這一列資料屬於哪一種類型。WordPress 安裝好就內建五種原生的文章類型,post(一般文章)、page(靜態頁面)、attachment(媒體附件)、revision(修訂版本)、nav_menu_item(導覽選單項目)。除此之外,區塊編輯器與區塊主題上線後,核心程式又自己用了幾個系統保留的類型,像是 wp_block(可重用區塊)、wp_template、wp_template_part、wp_global_styles、wp_navigation,這些名稱之後不能拿來當自訂文章類型的 key。
自訂文章類型(Custom Post Type,簡稱 CPT)就是開發者自己動手,把一種新的內容類型登記進這套系統,讓它跟 post、page 平起平坐,擁有自己的欄位設定、網址結構、後台清單頁。它不是外掛裝一裝就自動出現的東西,而是要透過 register_post_type() 這支函式主動註冊,註冊完那個類型的資料一樣寫進 wp_posts,只是 post_type 欄位存的值換成你取的 key,例如 portfolio。
很多人第一次遇到「這批內容要不要獨立」的需求時,最容易把 CPT、分類、自訂欄位三件事混在一起。分類與標籤處理的是「同一種內容裡怎麼歸類」,作品跟文章都還是同一種東西(post),只是貼了不同標籤區分主題;自訂欄位處理的是「同一篇內容要多存哪些額外資料」,例如一篇文章想多存一個「難度」欄位;自訂文章類型處理的則是「這根本是不同種內容」,它有自己的欄位組合、自己的網址規則、自己的後台清單頁,不會跟一般文章擠在同一份列表裡。判斷的關鍵不是內容主題不一樣,而是這批內容需不需要一整套獨立的結構。

決定該不該開新文章類型的關鍵在於要不要獨立的清單頁
怎麼判斷這批內容該不該獨立成一個新的文章類型?三個判準可以依序問過一輪,不必憑感覺。
- 這批內容要不要有自己的網址結構與封存頁,如果希望使用者能直接瀏覽一頁「作品集」的清單,網址規則跟一般文章的清單頁分開,這就是明顯的訊號;如果只是想在文章列表裡多一個篩選條件,分類就夠用了。
- 欄位需求跟一般文章差多少,一般文章帶的欄位組合是標題、內容編輯器、作者、特色圖片、摘要、迴響這一套,如果新內容其實用得到跟文章一樣的欄位,往往代表分類或自訂欄位就能解決,不必另開一種類型;反過來,如果這批內容根本不需要摘要或迴響,卻需要案主名稱、完成年份這類專屬欄位,就是在往 CPT 的方向靠。
- 管理介面要不要獨立清單,作品跟部落格文章混在同一份清單裡,數量一多,找起來會愈來愈麻煩,需要一份自己的清單頁、自己的篩選條件,也是開新類型的理由。
反面案例是很多人開 CPT 只是想多一個維度,例如網站的文章想同時用「難度」跟「地區」兩種方式分類,這種情境其實只要多加一個分類法(taxonomy)掛在既有的文章類型上就處理得掉,完全不需要動到文章類型本身。三個判準只要有一項明顯成立,通常就值得開一個新的文章類型;三項都答不上來,多半是分類或自訂欄位就夠用。

判準都問過一輪、確定要開新的文章類型之後,第一個要決定的不是參數要怎麼寫,而是這段註冊程式碼該放在哪裡,放的位置會直接影響這批內容以後好不好維護。
子布景主題、外掛與程式碼片段外掛的取捨
註冊 register_post_type() 的程式碼,常見有三個放置的地方,各自的取捨不太一樣。
最直接的做法是寫進子布景主題的 functions.php。這樣做上手最快,改完存檔立刻生效,適合快速測試一個構想、不打算長期維護的情境。缺點是這段程式碼跟目前用的布景主題綁在一起,換主題、或換回原本的主題時,這段註冊邏輯會跟著消失,WordPress 不再認得這個文章類型,於是不知道該怎麼顯示它。內容其實還留在資料庫裡,只是暫時找不到對應的規則去呈現,這種內容還在、規則不見了的狀態最容易讓人誤以為資料整批不見了。
比較穩妥的做法是把註冊邏輯寫成一支獨立的小外掛,跟布景主題完全脫鉤。換主題的時候外掛照樣啟用,這批內容不會受影響,這也是官方文件與多數開發資源建議的長期做法,尤其這批內容打算長期經營、而不只是這次改版的實驗性功能時,更值得一開始就用這個方式。
程式碼片段外掛,例如常見的 Code Snippets 這類外掛,是第三個選項,適合不想自己動手建立外掛檔案結構、卻仍然希望用程式碼精準掌控每個參數的情況。它讓你透過後台介面直接貼上程式碼、按個開關就能啟用或停用,不需要碰檔案系統,缺點是網站多了一個外掛依賴,一旦停用或移除這支外掛,貼在裡面的程式碼也會跟著失效。
不管選哪一種,有一條規則三者都要遵守,註冊的函式一定要掛在 init 這個動作鉤子(action hook)上,而且不能在 init 觸發之前就執行註冊。WordPress 官方文件對這點寫得很明確,文章類型的註冊不應該掛在 init 之前,這是因為 WordPress 核心系統要先把自己該準備的東西準備好,才輪到外掛跟主題把自訂的文章類型掛進去,提早註冊容易踩到核心還沒初始化完成的時序問題。
放置的位置決定好之後,才輪到把 register_post_type() 這支函式本身的參數一個一個拆開來看。
register_post_type() 的核心參數和必填欄位
register_post_type() 只吃兩個參數,第一個是這個文章類型的 key(一段英文字串),第二個是設定用的陣列,把所有細節都塞進這個陣列裡。整段註冊邏輯通常包在一個自訂函式裡,再用 add_action('init', '你的函式名稱') 掛進 init 這個動作鉤子,呼應上一節提到的規則。
最少幾行就能跑起來,先看一個最小可動的版本。
function pongo_register_portfolio() {
$args = array(
'label' => '作品',
'public' => true,
);
register_post_type( 'portfolio', $args );
}
add_action( 'init', 'pongo_register_portfolio' );Code language: PHP (php)
這幾行就足以讓 WordPress 認得一個叫 portfolio 的新文章類型,後台也會出現對應的選單。但只給 label 跟 public 只是最基本的骨架,實際上線通常還要再加幾個常用參數,讀完能自己組出一份完整可用的註冊陣列,而不是複製貼上就算交差。
post_type 命名的字元限制和保留字陷阱
取這個文章類型的 key 時,最容易踩的第一個雷是字元數。這個值最終會寫進資料庫 wp_posts 資料表的 post_type 欄位,而這個欄位的長度上限是 20 個字元,超過這個長度,register_post_type() 會直接回傳錯誤,慣例上也只用小寫英文字母、數字跟底線,不要夾雜大寫或空白。
第二個容易踩的雷是撞名。前面提過 WordPress 內建有 post、page、attachment、revision、nav_menu_item 這五種原生類型,加上區塊編輯器時代新增的系統保留類型,例如 wp_block、wp_template 這幾個,取名時務必避開這些字串。撞名不會跳出明顯的錯誤訊息提醒你,而是這個文章類型的行為會變得異常,甚至直接被系統既有的類型覆蓋掉,排查起來會比字元超限更花時間,因為表面上程式碼看起來完全正常。
labels 陣列決定後台介面顯示的每一句文字
labels 這個子陣列不影響任何功能運作,純粹決定後台介面上看到的文字,左側選單的名稱、「新增文章」按鈕的字樣、搜尋框的提示語,全都是靠這個陣列控制。沒有設定 labels 的話,WordPress 不會留白,而是直接沿用內建「文章」的措辭,於是後台選單明明是作品集功能,顯示的卻是「文章」「新增文章」這種字眼,讀起來很奇怪,也容易讓使用這個後台的人搞混。
最常被忽略、卻最容易一眼看出沒設定的幾個 key 是 name(複數名稱)、singular_name(單數名稱)、menu_name(左側選單顯示的名稱)跟 add_new_item(新增畫面的標題)。把這幾個換成貼合這批內容的字眼,例如 singular_name 設成「作品」、add_new_item 設成「新增作品」,後台介面立刻會跟這個文章類型的性質對上。WordPress 官方文件也說明,沒設定 labels 時,非階層式的文章類型會沿用一般文章的介面字樣,階層式的則沿用頁面的字樣,階層式與否由另一個參數 hierarchical 決定,這也是為什麼很多剛接觸 CPT 的人會在後台看到「文章」或「頁面」的殘影,其實是這個機制在起作用,不是設定漏掉造成的錯誤。
public 一個開關牽動後台可見度與前台查詢三件事
public 常被誤以為只是要不要在前台顯示,但它實際上是好幾個更細緻開關的預設值來源。這個參數預設是否(false),一旦設為真,show_ui(後台要不要顯示管理介面)、publicly_queryable(前台能不能被查詢到)、exclude_from_search(取反,也就是要不要被排除在搜尋結果外)這幾個附屬參數,只要沒有另外明確覆寫,就會跟著一起打開。
這層預設關係平常不太需要在意,直接把 public 設成 true,多數情境就已經涵蓋到需要的行為。真正需要留意的時機,是遇到比較細緻的需求,例如這批內容要出現在後台方便管理,但不想被前台搜尋找到,這時候就該分別單獨設定 show_ui、exclude_from_search 這幾個參數,而不是整組都跟著 public 走。搞懂 public 跟附屬參數的這層繼承關係,之後看到某些細節行為跟預期對不上,例如某個文章類型明明設了 public 為真,卻搜尋不到,才知道該去哪一個附屬參數裡找答案。
supports 陣列決定編輯畫面保留的區塊
supports 決定編輯畫面上會出現哪些功能區塊,可用的選項包含 title(標題)、editor(內容編輯器)、author(作者)、thumbnail(特色圖片)、excerpt(摘要)、trackbacks、custom-fields(自訂欄位)、comments(迴響)、revisions(修訂)、page-attributes(頁面屬性)、post-formats(文章格式)。依照這批內容實際需要的欄位去挑,例如作品集通常需要特色圖片來放代表作品的圖,但不太需要迴響功能,把 comments 拿掉即可。
沒有指定 supports 這個參數時,WordPress 預設只給 title 跟 editor 兩項,這一點常被誤會成什麼都沒有,其實是有的,只是只保留最基本的兩項。如果把整個 supports 參數設成 false,連標題與編輯器都會被拿掉,這通常只在完全靠自訂欄位運作、不需要標準內容編輯器的特殊情境才會這樣設,一般的作品集、服務項目這類內容很少用到這麼極端的設定。另外要留意的是,thumbnail 這個選項要真正運作,佈景主題本身也要有支援特色圖片的宣告(add_theme_support('post-thumbnails')),只在文章類型這邊加了 thumbnail,佈景主題那邊沒配合,特色圖片欄位一樣不會出現,這是讀者實測時最容易遇到問題、卻又最容易漏講的一步。
has_archive 與 rewrite 一起決定前台網址結構
這兩個參數合起來決定這個文章類型在前台長什麼網址、有沒有一頁列表可以瀏覽。has_archive 設為真,會啟用這個文章類型的封存頁,也就是列表頁,對應的網址預設是「你的網站網址/文章類型 key/」;rewrite 這個陣列則可以進一步改變單篇跟列表頁的網址前綴,slug 決定網址要用哪個字串當前綴,with_front 決定要不要沿用目前固定連結結構已經設定好的前綴。
這組參數是讀者最容易搞混的地方,也是找不到剛登記好的文章類型時,兩個常見狀況的根源之一。舉例來說,如果只設了 has_archive 為真,卻沒有另外調整 rewrite,列表頁會直接沿用文章類型 key 當網址前綴,想要換成更好記的字串,就得靠 rewrite 的 slug 去改。不管是剛加上 has_archive,還是改了 rewrite 的任何一項,改完之後都要重新整理一次永久連結才會生效。
show_in_rest 打開區塊編輯器和 REST API 支援
show_in_rest 是很多年份比較久的教學會漏掉、但現在幾乎一定要設的一個參數。WordPress 官方文件對這個參數的作用寫得很直接,是否把這個文章類型納入 REST API,而要讓這個文章類型能用區塊編輯器編輯,這個參數必須設為真。沒開這一項,這個文章類型的編輯畫面會退回舊版的經典編輯器,跟網站其他地方用的區塊編輯體驗不一致,讀者第一次遇到往往會以為自己哪裡設錯,其實只是少了這一行。
沒開 show_in_rest,也代表這個文章類型的資料沒辦法透過 WordPress REST API 讀寫,如果之後想用前端框架或行動裝置應用程式串接這批內容,一樣沒辦法進行。另外還有一個可以搭配調整的參數 rest_base,用來自訂這個文章類型在 REST API 路徑裡的名稱,沒設定的話預設會沿用文章類型的 key,多數情境維持預設就夠用,除非 key 本身不好記,才需要另外指定一個更直覺的路徑名稱。
註冊後永久連結需要重新整理一次才會生效
整篇教學裡最短、卻最多人漏掉的一步,就是文章類型註冊完,永久連結要重新整理一次,網址才會真的打得開。前面幾個參數都設對了,程式碼也沒有錯字,封存頁卻打開就是 404,原因往往不是程式碼,而是漏了這一步。
原因在於 WordPress 內部用一份網址對照規則表,決定哪個網址該對應到哪一種內容,這份表不是每次讀取頁面都重新計算,而是先算好存起來,等網址被造訪時直接查表比對。新增一個文章類型、或改了 rewrite 相關設定時,這份對照表不會自己知道多了新的規則,還是照舊版的表在比對,於是新的網址查不到對應規則,回應就是 404。
解法很簡單,不需要碰任何程式碼,登入後台,到「設定」>「永久連結」,什麼都不用改,直接按一次「儲存變更」,WordPress 就會重新產生一份新的對照表,把剛剛註冊的文章類型也算進去。開發階段每加一個新的文章類型,或者每改一次 rewrite 相關的設定,都要重做這個動作,忘記做,就是「封存頁打不開」最常見的原因。
找不到剛登記好的文章類型通常卡在兩個地方
註冊完、也重新整理過永久連結,結果還是找不到剛登記好的文章類型,這種狀況實際遇到時,幾乎都卡在下面兩個地方之一,一個是前台,一個是後台,各自對應到不同的檢查點。
封存頁出現 404,多半是永久連結沒重新整理或網址撞名
第一種狀況是前台的封存頁打開變成 404。最常見的原因就是上一節講的,剛註冊完或改過 rewrite 設定之後,忘了到「設定」>「永久連結」按一次儲存,對照表沒更新,網址自然查不到對應規則。
第二種原因比較隱性,這個文章類型的網址前綴(slug)跟網站上既有的某個頁面撞名了。舉例來說,如果網站原本就有一個網址是「你的網站網址/portfolio/」的靜態頁面,這時候又把作品集這個文章類型的網址前綴也設成 portfolio,WordPress 判斷網址時會出現衝突,行為變得不穩定。遇到這種情況,解法是把 CPT 的 rewrite 裡的 slug 改成一個跟既有頁面不同的字串,例如把作品集的網址前綴改成 works 或 portfolios,避開撞名,問題通常就解決了。
選單不出現,通常出在 show_in_menu 或 capability_type 沒對齊
第二種狀況是後台側邊欄根本沒看到這個文章類型的選單項目,不是前台 404,而是連編輯畫面都進不去。第一個檢查點是 show_ui 跟 show_in_menu 是不是都設成真,WordPress 官方文件明確寫出,show_in_menu 要生效,前提是 show_ui 也必須是真,只設了 show_in_menu 卻漏掉 show_ui,選單一樣不會出現。
第二個檢查點跟權限有關。如果自訂了 capability_type 這個參數,卻沒有搭配 map_meta_cap 或對應的角色權限設定,目前登入的帳號很可能根本沒有權限看到這個文章類型的管理介面,表面上看起來就像選單消失了,其實是權限判斷把它擋在外面。capability_type 用來建構讀取、編輯、刪除這幾種權限字串,map_meta_cap 則決定要不要啟用內建的權限對應邏輯,兩者沒對齊,最常見的症狀就是管理員以外的帳號登入後台完全看不到這個文章類型,管理員自己測試卻一切正常,容易讓人誤以為程式碼沒問題,其實是漏了權限設定這一環。
CPT UI 外掛能省下整段 register_post_type() 程式碼
前面幾節教的是自己寫程式碼註冊文章類型,如果不想寫程式碼,也還有現成的路可以走。Custom Post Type UI,簡稱 CPT UI,是一支廣泛被使用的免費外掛,透過後台介面填欄位就能建立文章類型與自訂分類法,完全不用碰程式碼;根據 WordPress 官方外掛目錄的說明,目前有超過一百萬個啟用中的網站在使用這支外掛。
CPT UI 的介面欄位其實就是把前面教的 register_post_type() 參數,原封不動轉成一份表單,標籤、是否公開、支援哪些功能區塊、要不要啟用封存頁,全都是同一批參數,只是換成勾選框跟輸入框呈現。前面把這些參數的原理都搞懂了,用外掛操作時每個欄位在管什麼、會連動哪些行為,也都能對上號,不會變成單純照著介面填、卻不知道為什麼要這樣填。改完存檔即完成註冊,跟自己寫程式碼一樣,改動後同樣需要重新整理一次永久連結才會生效,這一步不會因為改用外掛就省略。
它還有一個對長期維護有幫助的功能,可以把後台設定好的文章類型匯出成對應的 PHP 程式碼。對想從介面操作過渡到自己寫程式碼維護的人來說,這是一條現成的橋,先用介面把設定調到滿意,再把匯出的程式碼搬進外掛或子布景主題裡,兩種做法不是互斥的兩條路,而是同一件事的兩種操作方式。
區塊主題把封存頁與單篇模板規則換了一套邏輯
前面講的網址、封存頁邏輯,是傳統佈景主題,也就是經典主題,的運作方式。如果網站用的是區塊主題,也就是全站編輯(FSE),前台這個文章類型的封存頁跟單篇頁該用哪個版面,判斷機制已經換了一套邏輯,沒搞懂這點,套用區塊主題時容易找不到熟悉的檔案,誤以為自己哪裡設錯。
傳統佈景主題用 PHP 樣板檔案決定版面,依照樣板階層規則去找對應的檔案,例如 archive-{post_type}.php 或 single-{post_type}.php,把 {post_type} 換成實際的文章類型 key,找不到對應的檔案,才會退回通用的 archive.php 或 single.php。這是 WordPress 行之有年的規則,前面幾節提到的封存頁、單篇頁,指的都是這套機制底下的版面。
區塊主題改用存放在主題 templates 目錄底下的 HTML 區塊樣板,不再是一個個 PHP 檔案,而是透過 theme.json 裡的設定,或是後台「外觀」>「編輯器」的樣板管理介面,指定某個樣板要對應到哪個文章類型。這套機制是 WordPress 核心自 5.9 版起導入全站編輯功能後既有的架構設計,不管用哪一種主題,這個文章類型能不能出現在對應的樣板選單裡,前提仍然是前面教的 show_in_rest 這類參數要設對,少了它,區塊編輯器與後台樣板功能相關的能力打不開,樣板選單自然也看不到這個文章類型可以選。
把這幾個參數跟排查步驟走過一輪之後,register_post_type() 就不再是複製貼上就交差的黑盒子,而是一份可以自己組、自己調的設定清單。往後每次要幫網站開一個新的自訂文章類型,不管是服務項目、案例研究還是常見問答,同一套判斷邏輯都能重複套用,先問要不要獨立的清單頁與網址,再決定放置的位置,最後把參數一項項對齊需求,剩下的就是重新整理永久連結,讓 WordPress 把新規則記住。等網站內容種類愈長愈多,這套自己動手註冊自訂文章類型的能力,會比每次都到外掛市集找現成方案更靠得住。
