EP086. “search.php 模板与按文章类型分类样式”
🔒 登录后可标记已读让传统搜索结果页脱离默认的 index.php(博客列表)样式,新建 search.php(WordPress 按命名约定会自动识别这个文件名当作搜索结果模板)。核心思路是用 get_template_part('template-parts/content', get_post_type()) 动态拼文件名,让每种文章类型(post/page/program/professor/campus/event)都能各自匹配一个专属的 template-parts/content-{类型}.php 局部模板,不用写一长串 if/else。顺带处理了搜索词的 XSS 安全转义、零结果提示,以及把搜索表单抽成 searchform.php(WordPress 认的另一个特殊文件名)避免代码重复。
涉及文件
wp-content/themes/fictional-university-theme/search.php(新建,复制自index.php再修改)wp-content/themes/fictional-university-theme/searchform.php(新建)wp-content/themes/fictional-university-theme/template-parts/content-post.php(新建,从search.php里原本的结果 HTML 剪切出来)wp-content/themes/fictional-university-theme/template-parts/content-professor.php(新建)wp-content/themes/fictional-university-theme/template-parts/content-program.php(新建)wp-content/themes/fictional-university-theme/template-parts/content-page.php(新建)wp-content/themes/fictional-university-theme/template-parts/content-campus.php(新建)
代码实现
search.php(完整文件):
<?php
get_header();
pageBanner(array(
'title' => 'Search Results',
'subtitle' => 'You searched for “' . esc_html(get_search_query(false)) . '”'
));
?>
<div class="container container--narrow page-section">
<?php
if (have_posts()) {
while(have_posts()) {
the_post();
get_template_part('template-parts/content', get_post_type());
}
echo paginate_links();
} else {
echo '<h2 class="headline headline--small-plus">No results match that search.</h2>';
}
get_search_form();
?>
</div>
<?php get_footer();
?>
searchform.php(新建,内容跟 EP085 里 page-search.php 手写的表单一致,抽出来给 get_search_form() 统一调用):
<form class="search-form" method="get" action="<?php echo esc_url(site_url('/')); ?>">
<label class="headline headline--medium" for="s">Perform a New Search:</label>
<div class="search-form-row">
<input placeholder="What are you looking for?" class="s" id="s" type="search" name="s">
<input class="search-submit" type="submit" value="Search">
</div>
</form>
template-parts/content-post.php(博客文章样式,从原本 index.php/search.php 里剪切出来):
<div class="post-item">
<h2 class="headline headline--medium headline--post-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<div class="metabox">
<p>Posted by <?php the_author_posts_link(); ?> on <?php the_time('n.j.y'); ?> in <?php echo get_the_category_list(', '); ?></p>
</div>
<div class="generic-content">
<?php the_excerpt(); ?>
<p><a class="btn btn--blue" href="<?php the_permalink(); ?>">Continue reading »</a></p>
</div>
</div>
template-parts/content-professor.php(复用 single-program.php 里教授关联列表的卡片结构):
<div class="post-item">
<li class="professor-card__list-item">
<a class="professor-card" href="<?php the_permalink(); ?>">
<img class="professor-card__image" src="<?php the_post_thumbnail_url('professorLandscape') ?>">
<span class="professor-card__name"><?php the_title(); ?></span>
</a>
</li>
</div>
template-parts/content-program.php / content-page.php / content-campus.php(都是 content-post.php 的简化版:去掉 metabox 作者信息,按钮文案各自不同):
<!-- content-program.php:按钮文案改成 View program -->
<div class="post-item">
<h2 class="headline headline--medium headline--post-title"><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h2>
<div class="generic-content">
<?php the_excerpt(); ?>
<p><a class="btn btn--blue" href="<?php the_permalink(); ?>">View program »</a></p>
</div>
</div>
<!-- content-campus.php:按钮文案改成 View campus,结构跟 content-program.php 一致 -->
<!-- content-page.php:按钮文案保留 Continue reading,结构跟 content-post.php 一致(只是去掉了 metabox 作者信息)-->
(content-event.php 沿用课程更早期已经建好的版本,这一讲没有改动。)
关键改动点:
get_template_part('template-parts/content', get_post_type())是这一讲的核心技巧:第一个参数是固定前缀,第二个参数动态传入当前循环文章的类型,WordPress 会自动拼成template-parts/content-{类型}.php去找对应文件,不用写一长串 if/else 判断类型get_search_query(false)的参数false表示关闭默认转义,因为紧接着外面手动包了一层esc_html()——如果两层都转义,特殊字符会被转义两次显示错乱;反过来,如果要把搜索词直接当 HTML 属性值用(比如塞进value="..."里),可以不加false,让函数自己处理转义,不需要再包esc_html()- 零结果判断用
if (have_posts()) { ... } else { ... }包住整个 while 循环和分页链接,不是在循环体内部判断——如果查询根本没有结果,have_posts()从一开始就是 false,直接走 else 分支输出提示文案 - 表单去重:
searchform.php是 WordPress 认的特殊文件名,get_search_form()会自动去读取它;page-search.php(EP085 建的搜索入口页)理论上也应该同步改成调用get_search_form(),但 EP085 建立时留在页面里的那份内联表单代码没有在这一讲被替换掉,等于目前page-search.php和searchform.php里存在同一份表单 HTML 的重复,如果想彻底去重,可以手动把page-search.php里的<form>也换成<?php get_search_form(); ?>
[截图:前台 /?s=关键词 的搜索结果页,展示不同文章类型(博客文章/教授/项目等)各自的卡片样式]
Hook / Function 速查
| 名称 | 类型 | 用途 |
|---|---|---|
get_template_part($slug, $name) | WP 内建 function | 按 {$slug}-{$name}.php 拼文件名并引入局部模板,找不到就退回 {$slug}.php |
get_post_type() | WP 内建 function | 获取当前循环文章的类型,动态拼模板文件名的关键 |
get_search_query($escape) | WP 内建 function | 获取当前搜索关键词,$escape 传 false 可关闭默认转义,方便自己控制转义方式 |
esc_html() | WP 内建 function | 转义字符串成安全的 HTML 文本,防止 XSS |
have_posts() | WP 内建 function | 判断当前查询是否还有(或是否有)结果 |
paginate_links() | WP 内建 function | 生成分页链接 |
get_search_form() | WP 内建 function | 输出 searchform.php 里定义的搜索表单,WordPress 按约定文件名自动识别 |
常见坑
- 直接把用户搜索词原样输出到页面而不转义——恶意访客可以在搜索框里输入
<script>标签尝试 XSS 攻击,必须用esc_html()(用于 HTML 文本内容)或让get_search_query()自带转义(用于 HTML 属性值)处理,不能漏掉 get_search_query()和esc_html()同时使用时忘记把get_search_query()的转义关掉(传false)——两层转义会导致引号、尖括号等特殊字符被转义两次,显示成乱码get_template_part()的第二个参数写死具体类型(比如直接写'event')——这样就退化回硬编码判断了,失去了这一讲要做「动态按类型分发模板」的意义
Sources
Udemy:
- Become a WordPress Developer: Unlocking Power With Code — Section 17, EP086