本文约 6,274 字,预计阅读 16 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:导航站看似简单,做起来全是细节
网址导航是最”看起来容易”的站点类型:不就是一堆链接排成格子吗?真动手就会发现一堆问题——链接放在文章里还是独立类型?分类层级怎么定?点击后是直接跳走还是走中间页?外链要不要加 nofollow?站点截图怎么自动生成?死链怎么发现?
很多人第一版是把所有链接写死在一个页面里。短期能用,等链接超过一百条就彻底失控:改一个 URL 要翻半天,新增分类要重排整页,无法做筛选也无法统计点击。正确的做法是把”一个网址”当成一条结构化数据来管理,用自定义文章类型承载,再配一个分类法做分组。这样后台就能像管理文章一样管理链接,还能复用 WordPress 现成的查询、缓存与权限体系。
原理分析:导航站的数据模型
一个导航条目至少包含这些字段:站点名称、网址、简介、分类、图标/截图、排序权重、是否推荐、点击次数。其中”分类”应当做成分类法(taxonomy)而不是自定义字段,因为分类法自带归档页与 URL,利于 SEO;”点击次数”和”排序权重”是典型的自定义字段。
跳转方式有三种,取舍如下:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 直接外链 | 最简单,无中间跳转 | 无法统计,外链权重直接传递 |
/go/id 中间页 |
可统计、可加提示 | 多一次跳转,需处理缓存 |
| JS 跳转 | 可拦截、可延迟 | 爬虫不易识别,体验略差 |
推荐用 /go/{id} 中间页 + wp_safe_redirect(),既能统计点击,又能在跳转前做一层安全提示(比如提示”即将离开本站”)。注意这里必须用 wp_safe_redirect() 而非 wp_redirect(),前者会校验目标域名是否在允许列表内,可避免开放重定向漏洞。
保姆级实操
步骤 1:注册站点 CPT 与导航分类法。
add_action( 'init', 'mimangfei_register_site_cpt' );
function mimangfei_register_site_cpt() {
register_post_type( 'nav_site', array(
'labels' => array(
'name' => '网址收录',
'singular_name' => '网址',
'add_new_item' => '添加网址',
),
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'sites', 'with_front' => false ),
'supports' => array( 'title', 'thumbnail' ),
'show_in_rest' => true,
'menu_icon' => 'dashicons-admin-links',
) );
register_taxonomy( 'nav_cat', 'nav_site', array(
'labels' => array(
'name' => '网址分类',
'singular_name' => '分类',
),
'public' => true,
'hierarchical' => true,
'rewrite' => array( 'slug' => 'sites/cat' ),
'show_in_rest' => true,
) );
}
步骤 2:加自定义字段(网址、简介、推荐)。
add_action( 'add_meta_boxes_nav_site', function () {
add_meta_box( 'nav_site_url', '站点信息', function ( $post ) {
wp_nonce_field( 'save_nav_site', 'nav_site_nonce' );
$url = get_post_meta( $post->ID, '_site_url', true );
$desc = get_post_meta( $post->ID, '_site_desc', true );
echo '<p><label>网址:<input type="url" name="site_url" value="' . esc_url( $url ) . '" style="width:80%"></label></p>';
echo '<p><label>简介:<textarea name="site_desc" rows="3" style="width:80%">' . esc_textarea( $desc ) . '</textarea></label></p>';
}, 'nav_site', 'normal', 'high' );
} );
add_action( 'save_post_nav_site', function ( $post_id ) {
if ( ! isset( $_POST['nav_site_nonce'] )
|| ! wp_verify_nonce( $_POST['nav_site_nonce'], 'save_nav_site' ) ) {
return;
}
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( isset( $_POST['site_url'] ) ) {
update_post_meta( $post_id, '_site_url', esc_url_raw( $_POST['site_url'] ) );
}
if ( isset( $_POST['site_desc'] ) ) {
update_post_meta( $post_id, '_site_desc', sanitize_text_field( $_POST['site_desc'] ) );
}
} );
步骤 3:注册 /go/{id} 跳转规则并计数。
add_action( 'init', function () {
add_rewrite_tag( '%go_id%', '([0-9]+)' );
add_rewrite_rule( '^go/([0-9]+)/?$', 'index.php?go_id=$matches[1]', 'top' );
} );
add_action( 'template_redirect', function () {
$id = (int) get_query_var( 'go_id' );
if ( ! $id ) {
return;
}
$url = get_post_meta( $id, '_site_url', true );
if ( ! $url ) {
wp_safe_redirect( home_url() );
exit;
}
$clicks = (int) get_post_meta( $id, '_site_clicks', true );
update_post_meta( $id, '_site_clicks', $clicks + 1 );
wp_safe_redirect( $url, 302 );
exit;
} );
步骤 4:写卡片模板。 在 archive-nav_site.php 里按分类循环输出。
<?php
$cats = get_terms( array( 'taxonomy' => 'nav_cat', 'hide_empty' => true ) );
foreach ( $cats as $cat ) :
$sites = get_posts( array(
'post_type' => 'nav_site',
'posts_per_page' => -1,
'meta_key' => '_site_order',
'orderby' => array( 'meta_value_num' => 'DESC', 'title' => 'ASC' ),
'tax_query' => array( array(
'taxonomy' => 'nav_cat',
'field' => 'term_id',
'terms' => $cat->term_id,
) ),
) );
?>
<section class="nav-block">
<h2><?php echo esc_html( $cat->name ); ?></h2>
<div class="nav-grid">
<?php foreach ( $sites as $s ) :
$url = get_post_meta( $s->ID, '_site_url', true );
$desc = get_post_meta( $s->ID, '_site_desc', true );
?>
<a class="nav-card" href="<?php echo esc_url( home_url( '/go/' . $s->ID ) ); ?>"
target="_blank" rel="noopener">
<span class="nav-card-icon">
<?php echo get_the_post_thumbnail( $s->ID, 'thumbnail' ); ?>
</span>
<strong><?php echo esc_html( get_the_title( $s ) ); ?></strong>
<small><?php echo esc_html( $desc ); ?></small>
</a>
<?php endforeach; ?>
</div>
</section>
<?php
endforeach;
步骤 5:刷新重写规则。 注册完 add_rewrite_rule 必须刷新一次,否则 /go/123 会 404。
wp rewrite flush --hard --path=/var/www/mimangfei
步骤 6:处理外链的 rel 属性。 出站链接统一加 noopener(安全)与 nofollow(不传递权重),可在输出卡片时直接写死,也可以用 wp_targeted_link_rel 过滤器统一处理。
常见踩坑
- 加了重写规则却没刷新,
/go/123始终 404。 - 用
wp_redirect()跳转到外部域名,触发开放重定向风险(应改用wp_safe_redirect())。 - 卡片里放了
target="_blank"却没加rel="noopener",新页面可通过window.opener操作原页面。 - 缓存插件把
/go/路径也缓存了,点击统计全部失效(需把go加入缓存排除规则)。 - 站点图标直接用对方 favicon 地址,对方改地址后全站图标裂开(应下载到媒体库)。
- 案例:某导航站上线时图省事把外链写成
href="https://目标站",半年后发现自己的搜索流量被一个采集站抢走——采集站整页搬走了导航内容,连外链一起搬。后来改成/go/id中间页 +nofollow,采集站搬走的就是一堆指向自己站内的/go/路径,反而给原站带来了入口。 - 分类层级做到三级,URL 变成
/sites/cat/a/b/c,又长又难收录。 - 死链从不检查,几百条链接里三成已经打不开。
总结
导航站是否做对,看两件事:后台能否在 30 秒内完成”新增一个网址并归类”,以及前台点击是否都能被统计到。行动清单:① 网址用独立 CPT + 层级分类法;② 分类层级不超过两级;③ 外链走 /go/id 中间页;④ 用 wp_safe_redirect() 而非 wp_redirect();⑤ 图标本地化;⑥ 每月跑一次死链检查。数据量上来后接着看 导航站链接批量导入、导航站收录与 SEO 与 WordPress 自定义字段教程。
