搜索全站
安装部署 32 分钟阅读

WordPress企业站产品中心搭建:CPT+分类法完整教程

企业站的产品不能全堆在页面里。本文从零搭建产品中心:注册产品 CPT 与分类法、配置归档与详情页模板、做参数字段与筛选、处理固定链接与分页,附可直接使用的完整代码。

U
UFO_PF 2019-04-02 · 阅读 32 分钟
Wo 安装部署
阅读提示
本文约 12,639 字,预计阅读 32 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:两百个产品堆在一个页面里

一个做工业配件的客户,原有官网把 200 多个产品全部写在一个「产品展示」页面里,用表格排版,图片是缩略图硬缩放。后果很具体:页面加载 8 秒;想找某个型号只能 Ctrl+F;搜索引擎只收录了这一个页面,200 个产品没有任何一个能被单独搜到。

改成产品中心后:每个产品独立一个页面、独立 URL、独立标题与描述;按品类分五个分类;加入筛选与搜索;半年后长尾词带来的询盘占了总量的一半。

这个改造的核心,就是把「页面里的内容」变成「结构化的内容类型」。WordPress 里对应的能力是自定义文章类型(CPT)+ 分类法(Taxonomy)。一旦产品成为独立内容类型,分页、筛选、搜索、结构化数据、批量编辑这些能力全部自动可用,不需要额外开发。

原理分析:CPT 与分类法各自解决什么

概念 作用 例子
文章类型(post type) 区分「这是什么内容」 post / page / product / case
分类法(taxonomy) 区分「它属于哪一类」 category / product_cat / case_industry
分类项(term) 分类下的具体值 控制器 / 传感器
模板层级 决定用哪个模板渲染 single-product.php / taxonomy-product_cat.php

WordPress 的模板层级是这套体系的关键。注册了 product 类型后,会自动按以下顺序找模板:single-product.php → single.php → singular.php → index.php;归档则是 archive-product.php → archive.php → index.php。不用写任何路由代码,建文件即可生效。

分类法分两种:hierarchical => true 的是分类(有父子层级,后台是勾选框),适合品类;false 的是标签(扁平),适合参数标签。产品中心通常两个都要:品类做层级分类,应用/材质/认证做扁平标签。

还有一个必知参数:has_archive。设为 true 才会生成 /products/ 这个归档页;设为字符串可以直接指定归档 slug,比如 'has_archive' => 'products'。

保姆级实操

步骤 1:用 WP-CLI 生成代码骨架,避免手写错误

wp scaffold post-type product --label="产品" --textdomain=mmf --plugin=mmf-core
wp scaffold taxonomy product_cat --post_types=product --label="产品分类" --textdomain=mmf --plugin=mmf-core

生成的函数签名和参数都是标准写法,比手写可靠。下面给出一份整合后的完整版,可直接放进功能插件。

步骤 2:注册产品类型与分类法

<?php
/**
 * 产品中心:内容类型与分类法
 * 放在功能插件里,换主题不影响
 */
add_action( 'init', 'mmf_register_product' );
function mmf_register_product() {
    register_post_type( 'product', array(
        'labels' => array(
            'name'          => '产品',
            'singular_name' => '产品',
            'add_new_item'  => '添加产品',
            'edit_item'     => '编辑产品',
            'all_items'     => '所有产品',
        ),
        'public'        => true,
        'has_archive'   => 'products',
        'rewrite'       => array( 'slug' => 'products', 'with_front' => false ),
        'menu_icon'     => 'dashicons-cart',
        'menu_position' => 21,
        'supports'      => array( 'title', 'editor', 'thumbnail', 'excerpt', 'page-attributes' ),
        'show_in_rest'  => true,
        'taxonomies'    => array( 'product_cat' ),
    ) );

    register_taxonomy( 'product_cat', 'product', array(
        'labels'       => array( 'name' => '产品分类', 'singular_name' => '产品分类' ),
        'public'       => true,
        'hierarchical' => true,
        'rewrite'      => array( 'slug' => 'products', 'with_front' => false, 'hierarchical' => true ),
        'show_in_rest' => true,
        'show_admin_column' => true,
    ) );

    // 扁平标签:应用场景
    register_taxonomy( 'product_tag', 'product', array(
        'labels'       => array( 'name' => '应用场景' ),
        'public'       => true,
        'hierarchical' => false,
        'rewrite'      => array( 'slug' => 'product-tag' ),
        'show_in_rest' => true,
    ) );
}

注意 with_front => false:不设的话,如果固定链接结构里带了 /blog 前缀,产品 URL 会变成 /blog/products/xxx,非常难看。hierarchical => true 在 rewrite 里能让子分类 URL 变成 /products/controller/x200/ 这种多级形式。

步骤 3:刷新固定链接

注册完必须刷新一次重写规则,否则前台 404:

wp rewrite flush --hard

代码层面则只在插件激活时刷:

<?php
register_activation_hook( __FILE__, function() {
    mmf_register_product();
    flush_rewrite_rules();
} );

不要挂在 init 上每次请求都刷,那是常见的性能杀手。

步骤 4:建归档模板 archive-product.php

<?php
get_header(); ?>

<main class="archive-product">
    <header class="archive-header">
        <h1><?php post_type_archive_title(); ?></h1>
        <?php the_archive_description( '<div class="archive-desc">', '</div>' ); ?>
    </header>

    <?php
    // 分类筛选导航:用链接而非 AJAX,可被收录
    $terms = get_terms( array(
        'taxonomy'   => 'product_cat',
        'hide_empty' => true,
        'parent'     => 0,
    ) );
    if ( ! is_wp_error( $terms ) && $terms ) : ?>
        <nav class="product-filter" aria-label="产品分类筛选">
            <a href="<?php echo esc_url( get_post_type_archive_link( 'product' ) ); ?>" class="is-current">全部</a>
            <?php foreach ( $terms as $term ) : ?>
                <a href="<?php echo esc_url( get_term_link( $term ) ); ?>">
                    <?php echo esc_html( $term->name ); ?>
                    <span class="count">(<?php echo (int) $term->count; ?>)</span>
                </a>
            <?php endforeach; ?>
        </nav>
    <?php endif; ?>

    <div class="product-grid">
        <?php if ( have_posts() ) : ?>
            <?php while ( have_posts() ) : the_post(); ?>
                <article class="product-card">
                    <a href="<?php the_permalink(); ?>">
                        <?php if ( has_post_thumbnail() ) {
                            the_post_thumbnail( 'mmf-card', array(
                                'loading' => 'lazy',
                                'alt'     => esc_attr( get_the_title() ),
                            ) );
                        } ?>
                        <h2><?php the_title(); ?></h2>
                    </a>
                    <?php
                    $model = get_post_meta( get_the_ID(), '_product_model', true );
                    if ( $model ) {
                        echo '<p class="model">型号:' . esc_html( $model ) . '</p>';
                    }
                    ?>
                </article>
            <?php endwhile; ?>
        <?php else : ?>
            <p>暂无产品。</p>
        <?php endif; ?>
    </div>

    <?php the_posts_pagination( array( 'mid_size' => 2 ) ); ?>
</main>

<?php get_footer();

the_posts_pagination() 会自动生成带 paginate_links() 的分页,且支持 /page/2/ 的规范 URL。

步骤 5:调整归档每页数量

主查询的数量不要用 WP_Query 另开一个循环,应该用 pre_get_posts:

<?php
add_action( 'pre_get_posts', 'mmf_product_archive_query' );
function mmf_product_archive_query( $query ) {
    if ( is_admin() || ! $query->is_main_query() ) {
        return;
    }

    if ( is_post_type_archive( 'product' ) || is_tax( 'product_cat' ) ) {
        $query->set( 'posts_per_page', 24 );
        $query->set( 'orderby', 'menu_order title' );
        $query->set( 'order', 'ASC' );
    }
}

用 menu_order 排序配合 page-attributes 支持,可以让客户在后台拖动排序(需安装排序插件或自行实现),比按发布时间更符合产品展示逻辑。

步骤 6:建详情页模板 single-product.php

<?php
get_header(); ?>

<main class="single-product">
    <?php while ( have_posts() ) : the_post(); ?>
        <article <?php post_class(); ?>>
            <h1><?php the_title(); ?></h1>

            <?php
            // 面包屑:首页 > 产品中心 > 分类 > 当前
            $terms = get_the_terms( get_the_ID(), 'product_cat' );
            if ( $terms && ! is_wp_error( $terms ) ) {
                $term = array_shift( $terms );
                echo '<nav class="breadcrumb"><a href="' . esc_url( home_url( '/' ) ) . '">首页</a> › '
                    . '<a href="' . esc_url( get_post_type_archive_link( 'product' ) ) . '">产品中心</a> › '
                    . '<a href="' . esc_url( get_term_link( $term ) ) . '">' . esc_html( $term->name ) . '</a> › '
                    . '<span>' . esc_html( get_the_title() ) . '</span></nav>';
            }
            ?>

            <div class="product-gallery">
                <?php the_post_thumbnail( 'mmf-wide', array( 'loading' => 'eager' ) ); ?>
            </div>

            <div class="product-content">
                <?php the_content(); ?>
            </div>

            <?php
            // 参数表
            $spec = get_post_meta( get_the_ID(), '_product_spec', true );
            if ( is_array( $spec ) && $spec ) : ?>
                <table class="product-spec">
                    <caption>技术参数</caption>
                    <tbody>
                    <?php foreach ( $spec as $row ) : ?>
                        <tr>
                            <th scope="row"><?php echo esc_html( $row['name'] ); ?></th>
                            <td><?php echo esc_html( $row['value'] ); ?></td>
                        </tr>
                    <?php endforeach; ?>
                    </tbody>
                </table>
            <?php endif; ?>

            <div class="product-cta">
                <?php echo do_shortcode( '[contact-form-7 id="123" title="产品询价"]' ); ?>
            </div>
        </article>

        <?php the_post_navigation( array(
            'prev_text' => '上一个产品:%title',
            'next_text' => '下一个产品:%title',
        ) ); ?>
    <?php endwhile; ?>
</main>

<?php get_footer();

首图用 loading => 'eager',因为详情页的 LCP 通常就是它;网格里的卡片图才用 lazy。

步骤 7:参数字段的存取

不用 ACF 也能做,用自定义字段 + 简单的元框:

<?php
add_action( 'add_meta_boxes', function() {
    add_meta_box( 'mmf-product-spec', '产品参数', 'mmf_spec_box', 'product', 'normal', 'default' );
} );

function mmf_spec_box( $post ) {
    wp_nonce_field( 'mmf_save_spec', 'mmf_spec_nonce' );
    $spec = get_post_meta( $post->ID, '_product_spec', true );
    $spec = is_array( $spec ) ? $spec : array();
    ?>
    <table class="form-table">
        <?php for ( $i = 0; $i < 8; $i++ ) :
            $name  = $spec[ $i ]['name'] ?? '';
            $value = $spec[ $i ]['value'] ?? ''; ?>
            <tr>
                <td><input type="text" name="mmf_spec_name[]" value="<?php echo esc_attr( $name ); ?>" placeholder="参数名" style="width:100%"></td>
                <td><input type="text" name="mmf_spec_value[]" value="<?php echo esc_attr( $value ); ?>" placeholder="参数值" style="width:100%"></td>
            </tr>
        <?php endfor; ?>
    </table>
    <?php
}

add_action( 'save_post_product', 'mmf_save_spec' );
function mmf_save_spec( $post_id ) {
    if ( ! isset( $_POST['mmf_spec_nonce'] )
        || ! wp_verify_nonce( sanitize_key( $_POST['mmf_spec_nonce'] ), 'mmf_save_spec' ) ) {
        return;
    }
    if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
        return;
    }
    if ( ! current_user_can( 'edit_post', $post_id ) ) {
        return;
    }

    $names  = isset( $_POST['mmf_spec_name'] ) ? (array) wp_unslash( $_POST['mmf_spec_name'] ) : array();
    $values = isset( $_POST['mmf_spec_value'] ) ? (array) wp_unslash( $_POST['mmf_spec_value'] ) : array();

    $spec = array();
    foreach ( $names as $i => $name ) {
        $n = sanitize_text_field( $name );
        $v = sanitize_text_field( $values[ $i ] ?? '' );
        if ( '' !== $n && '' !== $v ) {
            $spec[] = array( 'name' => $n, 'value' => $v );
        }
    }

    update_post_meta( $post_id, '_product_spec', $spec );
}

字段名带下划线前缀(_product_spec)在后台自定义字段列表里默认隐藏,界面更干净。

步骤 8:批量导入与验证

# 批量导入(配合 wp post create 循环,或用官方 importer)
wp post list --post_type=product --fields=ID,post_title,post_name --format=csv > /tmp/products.csv

# 验证 URL 与分页
curl -sI https://example.com/products/ | head -3
curl -sI https://example.com/products/page/2/ | head -3

常见踩坑

  1. CPT 声明在主题里:换主题后全部 404。必须放插件(判断依据见主题迁移相关实践)。
  2. 注册后不刷新固定链接:典型症状是归档页正常、详情页 404。
  3. 归档页用新建 WP_Query 覆盖主查询:分页参数失效,第二页永远显示第一页的内容。必须用 pre_get_posts。
  4. 分类法 slug 与 CPT slug 冲突:都写 products 时,子分类和单个产品可能互相抢 URL。让分类 rewrite 带 hierarchical => true 可以缓解,但不绝对,必要时给分类单独 slug。
  5. 案例: 某站点把产品 CPT 的 has_archive 设为 true,固定链接结构是 /%category%/%postname%/,结果产品 URL 变成 /uncategorized/xxx/。修法是 rewrite 加 'with_front' => false 并显式指定 slug。教训:with_front 参数在非默认固定链接结构下必配。
  6. 参数字段存成序列化数组后直接搜索:WP_Query 的 meta_query 对序列化数组只能做 LIKE 匹配,效率极低且不准确。需要筛选的参数应做成独立字段或分类法。
  7. 详情页首图加了 loading="lazy":LCP 变差,Lighthouse 分数下降。首图必须是 eager。

总结

产品中心的搭建步骤:注册 CPT 与分类法(放插件)→ 刷新固定链接 → 写 archive-product.php(含分类筛选)→ 用 pre_get_posts 调每页数量 → 写 single-product.php(含面包屑与参数表)→ 做参数元框 → 批量导入并验证 URL 与分页。

验收标准:每个产品有独立 URL 且可单独收录;归档页分页正常;分类页 URL 语义清晰;详情页首图未被懒加载;所有输出点都做了转义。

产品页结构定好后再做案例模块,两者通过「产品 ↔ 案例」互链形成内链主干;性能方面参考 WordPress主题资源精简清单 控制列表页的请求数,模板安全写法见 WordPress主题安全模板写法。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。