在技术博客和开发文档中,清晰的结构对于阅读体验至关重要。类比 Google 开发者文档和各大开源框架(如 Vue、React)的官方文档,经典的三栏式布局被证明是最为高效的阅读形态:
左侧:同类文章/系列导航,方便在相关主题间快速穿梭。
中间:专注的正文阅读区域。
右侧:当前页面的大纲目录(TOC),支持滚动高亮与快速锚点跳转。
以下就来实现一个为 WordPress 定制的插件 —— HMW Doc Layout。其能自动将特定文章类型调整为三栏式布局。
一、 功能亮点
零配置:安装即用,自动解析文章层级并生成目录。
智能标题 ID 注入:自动提取正文中的 <h2> 和 <h3> 标签,若无 id 属性则自动编排生成。
同类文章识别:
优先识别 WordPress 自带的分类法(Taxonomy)。
若无分类,则通过智能正则匹配标题前缀(例如 Python | 基础教程),自动将同系列文章归组。
现代化 UI 与滚动监听:
基于 CSS Grid 布局,并配合 position: sticky 实现侧边栏随页面滚动的粘性定位。
利用浏览器的 IntersectionObserver API,实现右侧大纲随阅读位置自动高亮。
完美响应式适配:在窄屏(移动设备)下自动折叠为单列流式布局,并支持原生 <details> 折叠展开交互。
二、详细解释
1. 插件基础信息与初始化检查
<?php
/**
* Plugin Name: HMW Doc Layout
* Description: Lab(project)单篇页面三栏布局...
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
const HMW_DOC_POST_TYPE = 'project';作用:定义插件头部信息,防止直接通过 URL 访问该 PHP 文件(ABSPATH 检查确保安全性)。
常量:定义了要应用此布局的 WordPress 文章类型(Post Type)为 project。
2. 左侧同类文章分组逻辑(hmw_doc_group_posts)
function hmw_doc_group_posts( WP_Post $post ) {
...
}作用:获取与当前文章属于同一“分类”或“系列”的其他文章,构建左侧导航。
匹配逻辑:
- 优先匹配分类/标签:遍历当前文章公开的 Taxonomy(分类法),若有分类,则获取该分类下的其他文章(最多 200 篇),按菜单顺序和日期排序。
- 回退匹配标题前缀:如果文章没有分类,则通过正则匹配标题前缀(例如
Python | 基础教程中的Python),把相同前缀的文章归为一类。
返回值:返回分组名称、文章列表数组、以及是否需要剥离标题前缀的布尔值。
3. 右侧本页目录(TOC)生成逻辑 (hmw_doc_build_toc)
function hmw_doc_build_toc( $content ) {
...
}作用:扫描文章正文中的 <h2> 和 <h3> 标签。
核心处理:
- 检查标题是否有自带
id,如果没有,则自动生成一个形如sec-1,sec-2的id并注入到 HTML 标签中。 - 将所有收集到的标题层级(Level)、
id和文本内容存入数组,用于在右侧渲染目录。
4. 正文包装与三栏 HTML 结构拼装 (hmw_doc_wrap_content)
function hmw_doc_wrap_content( $content ) {
...
}
add_filter( 'the_content', 'hmw_doc_wrap_content', 20 );作用:挂载到 WordPress 的 the_content 过滤器上,在输出正文时将其包裹进三栏网格结构中。
逻辑控制:
- 判断当前是否为目标文章类型的单篇页面,避免在首页或列表页生效。
- 调用前面两个函数获取左侧同类文章和右侧目录。
- 如果既没有同类文章又没有目录,则保持原样;否则生成带有
.hmw-doc类的三栏 HTML 结构(nav左侧、div中间正文、aside右侧目录)。
5. 样式设计 (hmw_doc_print_css)
function hmw_doc_print_css() {
...
}
add_action( 'wp_head', 'hmw_doc_print_css' );作用:通过 wp_head 注入 CSS 样式,控制三栏布局的外观。
关键样式特点:
- 网格布局:使用 CSS Grid (
grid-template-columns: 240px minmax(0, 1fr) 220px;) 实现固定两侧、自适应中间的正文。 - 粘性定位:左右两侧目录使用
position: sticky;,在页面滚动时会固定在视口顶部。 - 响应式适配:当屏幕宽度小于
1023px时,自动转为单列布局,并将左右目录变成可折叠的<details>元素,适应移动端阅读。
6. 前端交互脚本 (hmw_doc_print_js)
function hmw_doc_print_js() {
...
}
add_action( 'wp_footer', 'hmw_doc_print_js' );作用:通过 wp_footer 注入 JavaScript 代码,增强前端体验。
核心功能:
- 移动端默认折叠:在窄屏设备下自动关闭左右目录的展开状态。
- 滚动高亮监听:利用现代浏览器的
IntersectionObserverAPI,当用户在阅读正文滚动页面时,自动检测当前进入视口的标题,并高亮右侧目录中对应的链接。
三、 完整源代码
新建一个 PHP 文件(以本站为例 hmw-doc-layout.php),将以下代码放入 WordPress 插件目录(wp-content/plugins/hmw-doc-layout/)中,在后台插件管理页面「激活」即可。
<?php
/**
* Plugin Name: HMW Doc Layout
* Description: Lab(project)单篇页面三栏布局:左侧「同类文章目录」,中间正文,右侧「本页目录」(类似 Google 开发者文档)。
* Version: 1.0.0
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/* 要应用的文章类型(Lab 的 URL 是 /project/,所以类型是 project) */
const HMW_DOC_POST_TYPE = 'project';
/**
* 取得「同类文章」。
* 1) 优先用该文章已有的分类/标签(taxonomy);
* 2) 没有的话,用标题前缀分组,例如「Python|…」「Lychee|…」。
*
* @return array [ 分组名称, 文章数组, 是否去掉标题前缀 ]
*/
function hmw_doc_group_posts( WP_Post $post ) {
$args = array(
'post_type' => $post->post_type,
'post_status' => 'publish',
'posts_per_page' => 200,
'orderby' => array(
'menu_order' => 'ASC',
'date' => 'ASC',
),
'no_found_rows' => true,
);
foreach ( get_object_taxonomies( $post->post_type, 'objects' ) as $tax ) {
if ( ! $tax->public ) {
continue;
}
$terms = get_the_terms( $post, $tax->name );
if ( $terms && ! is_wp_error( $terms ) ) {
$term = reset( $terms );
$args['tax_query'] = array(
array(
'taxonomy' => $tax->name,
'field' => 'term_id',
'terms' => $term->term_id,
),
);
return array( $term->name, get_posts( $args ), false );
}
}
if ( preg_match( '/^(.+?)\s*[||]/u', get_the_title( $post ), $m ) ) {
$prefix = trim( $m[1] );
$regex = '/^' . preg_quote( $prefix, '/' ) . '\s*[||]/u';
$all = get_posts( $args );
$same = array();
foreach ( $all as $p ) {
if ( preg_match( $regex, get_the_title( $p ) ) ) {
$same[] = $p;
}
}
return array( $prefix, $same, true );
}
return array( '', array(), false );
}
/**
* 给 h2/h3 补上 id,并收集目录。
*/
function hmw_doc_build_toc( $content ) {
$toc = array();
$content = preg_replace_callback(
'/<h([23])([^>]*)>(.*?)<\/h\1>/isu',
function ( $m ) use ( &$toc ) {
$text = trim( wp_strip_all_tags( $m[3] ) );
if ( '' === $text ) {
return $m[0];
}
$attrs = $m[2];
if ( preg_match( '/\sid=["\']([^"\']+)["\']/i', $attrs, $idm ) ) {
$id = $idm[1];
} else {
$id = 'sec-' . ( count( $toc ) + 1 );
$attrs .= ' id="' . esc_attr( $id ) . '"';
}
$toc[] = array(
'level' => (int) $m[1],
'id' => $id,
'text' => $text,
);
return '<h' . $m[1] . $attrs . '>' . $m[3] . '</h' . $m[1] . '>';
},
$content
);
return array( $content, $toc );
}
/**
* 包装正文。
*/
function hmw_doc_wrap_content( $content ) {
static $done = false;
if ( $done || is_feed() || is_admin() || ! is_singular( HMW_DOC_POST_TYPE ) ) {
return $content;
}
$post = get_post();
if ( ! $post || get_queried_object_id() !== $post->ID ) {
return $content;
}
$done = true;
list( $content, $toc ) = hmw_doc_build_toc( $content );
list( $label, $siblings, $strip ) = hmw_doc_group_posts( $post );
$has_side = count( $siblings ) > 1;
$has_toc = count( $toc ) > 1;
if ( ! $has_side && ! $has_toc ) {
return $content;
}
$classes = 'hmw-doc alignwide';
if ( ! $has_side ) {
$classes .= ' no-side';
}
if ( ! $has_toc ) {
$classes .= ' no-toc';
}
$out = '<div class="' . esc_attr( $classes ) . '">';
/* 左:同类文章 */
if ( $has_side ) {
$out .= '<nav class="hmw-doc__side" aria-label="同类文章">';
$out .= '<details class="hmw-doc__fold" open><summary>' . esc_html( $label ? $label : '同类文章' ) . '</summary>';
if ( $label ) {
$out .= '<p class="hmw-doc__label">' . esc_html( $label ) . '</p>';
}
$out .= '<ul>';
foreach ( $siblings as $p ) {
$title = get_the_title( $p );
if ( $strip ) {
$title = trim( preg_replace( '/^.+?\s*[||]\s*/u', '', $title, 1 ) );
}
$current = ( $p->ID === $post->ID );
$out .= '<li' . ( $current ? ' class="is-current"' : '' ) . '>';
$out .= '<a href="' . esc_url( get_permalink( $p ) ) . '"' . ( $current ? ' aria-current="page"' : '' ) . '>' . esc_html( $title ) . '</a></li>';
}
$out .= '</ul>';
$archive = get_post_type_archive_link( $post->post_type );
if ( $archive ) {
$out .= '<p class="hmw-doc__back"><a href="' . esc_url( $archive ) . '">← 返回 Lab</a></p>';
}
$out .= '</details></nav>';
}
/* 中:正文 */
$out .= '<div class="hmw-doc__main">' . $content . '</div>';
/* 右:本页目录 */
if ( $has_toc ) {
$out .= '<aside class="hmw-doc__toc" aria-label="本页目录">';
$out .= '<details class="hmw-doc__fold" open><summary>本页内容</summary>';
$out .= '<p class="hmw-doc__label">本页内容</p><ul>';
foreach ( $toc as $item ) {
$out .= '<li class="lv' . (int) $item['level'] . '"><a href="#' . esc_attr( $item['id'] ) . '">' . esc_html( $item['text'] ) . '</a></li>';
}
$out .= '</ul></details></aside>';
}
$out .= '</div>';
return $out;
}
add_filter( 'the_content', 'hmw_doc_wrap_content', 20 );
/**
* 样式。
*/
function hmw_doc_print_css() {
if ( ! is_singular( HMW_DOC_POST_TYPE ) ) {
return;
}
?>
<style id="hmw-doc-css">
html { scroll-behavior: smooth; }
.hmw-doc {
display: grid;
grid-template-columns: 240px minmax(0, 1fr) 220px;
gap: 2.5rem;
align-items: start;
}
.hmw-doc.no-side { grid-template-columns: minmax(0, 1fr) 220px; }
.hmw-doc.no-toc { grid-template-columns: 240px minmax(0, 1fr); }
.hmw-doc.no-side.no-toc { grid-template-columns: minmax(0, 1fr); }
.hmw-doc__main { min-width: 0; max-width: 780px; }
.hmw-doc__main h2,
.hmw-doc__main h3 { scroll-margin-top: 5rem; }
.hmw-doc__side,
.hmw-doc__toc {
position: sticky;
top: 2rem;
max-height: calc(100vh - 4rem);
overflow-y: auto;
font-size: 0.875rem;
line-height: 1.5;
}
body.admin-bar .hmw-doc__side,
body.admin-bar .hmw-doc__toc { top: 4rem; }
.hmw-doc__side { grid-column: 1; }
.hmw-doc__main { grid-column: 2; }
.hmw-doc__toc { grid-column: 3; }
.hmw-doc.no-side .hmw-doc__main { grid-column: 1; }
.hmw-doc.no-side .hmw-doc__toc { grid-column: 2; }
.hmw-doc ul { list-style: none; margin: 0; padding: 0; }
.hmw-doc__fold > summary { display: none; cursor: pointer; font-weight: 600; padding: 0.6rem 0; }
.hmw-doc__label {
margin: 0 0 0.75rem;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.04em;
opacity: 0.6;
}
/* 左侧目录 */
.hmw-doc__side li a {
display: block;
padding: 0.4rem 0.75rem;
border-left: 2px solid transparent;
color: inherit;
text-decoration: none;
opacity: 0.75;
}
.hmw-doc__side li a:hover { opacity: 1; background: color-mix(in srgb, currentColor 6%, transparent); }
.hmw-doc__side li.is-current a {
opacity: 1;
font-weight: 600;
border-left-color: currentColor;
background: color-mix(in srgb, currentColor 8%, transparent);
}
.hmw-doc__back { margin-top: 1rem; font-size: 0.8125rem; }
.hmw-doc__back a { color: inherit; opacity: 0.6; text-decoration: none; }
.hmw-doc__back a:hover { opacity: 1; }
/* 右侧目录 */
.hmw-doc__toc ul { border-left: 1px solid color-mix(in srgb, currentColor 15%, transparent); }
.hmw-doc__toc li a {
display: block;
padding: 0.3rem 0.75rem;
margin-left: -1px;
border-left: 2px solid transparent;
color: inherit;
text-decoration: none;
opacity: 0.65;
}
.hmw-doc__toc li.lv3 a { padding-left: 1.5rem; font-size: 0.8125rem; }
.hmw-doc__toc li a:hover { opacity: 1; }
.hmw-doc__toc li a.is-active { opacity: 1; font-weight: 600; border-left-color: currentColor; }
/* 窄屏:单列,两个目录折叠 */
@media (max-width: 1023px) {
.hmw-doc,
.hmw-doc.no-side,
.hmw-doc.no-toc { display: block; }
.hmw-doc__side,
.hmw-doc__toc {
position: static;
max-height: none;
margin-bottom: 1.25rem;
border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
}
.hmw-doc__fold > summary { display: block; }
.hmw-doc__fold .hmw-doc__label { display: none; }
.hmw-doc__main { max-width: none; }
}
</style>
<?php
}
add_action( 'wp_head', 'hmw_doc_print_css' );
/**
* 脚本:窄屏默认折叠 + 右侧目录滚动高亮。
*/
function hmw_doc_print_js() {
if ( ! is_singular( HMW_DOC_POST_TYPE ) ) {
return;
}
?>
<script id="hmw-doc-js">
(function () {
var doc = document.querySelector('.hmw-doc');
if (!doc) return;
if (window.matchMedia('(max-width: 1023px)').matches) {
doc.querySelectorAll('details.hmw-doc__fold').forEach(function (d) {
d.removeAttribute('open');
});
}
var links = [].slice.call(doc.querySelectorAll('.hmw-doc__toc a'));
if (!links.length || !('IntersectionObserver' in window)) return;
var map = {};
var heads = [];
links.forEach(function (a) {
var id = decodeURIComponent(a.getAttribute('href').slice(1));
map[id] = a;
var h = document.getElementById(id);
if (h) heads.push(h);
});
function setActive(id) {
links.forEach(function (a) { a.classList.remove('is-active'); });
if (map[id]) map[id].classList.add('is-active');
}
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (e.isIntersecting) setActive(e.target.id);
});
}, { rootMargin: '0px 0px -75% 0px' });
heads.forEach(function (h) { io.observe(h); });
if (heads.length) setActive(heads[0].id);
})();
</script>
<?php
}
add_action( 'wp_footer', 'hmw_doc_print_js' );
使用与定制
修改文章类型:如果你想将此布局应用到默认的 post(文章)或其他自定义文章类型,只需修改代码开头的常量:
const HMW_DOC_POST_TYPE = 'post'; // 改为你的目标 Post Type主题配合:建议配合 WordPress 主题的宽幅布局(alignwide 支持)使用,效果更佳。
标题规范:在编写文章时,保持良好的 H2、H3 语义化层级,插件便会自动生成精准的右侧导航目录。