1 From “Single-Article Reading” to “Continuous Reading”
A major difference between my blog and traditional personal blogs is the existence of many highly “continuous” series articles: for example, the “Cloudflare Series”, the “Awakening of Voice” series, and also the “Building a ‘Lightweight Knowledge Index’ for My Blog” series in which this article is situated.
As this type of content grows, I have gradually come to realize that for a blog, “getting users to enter an article” is not particularly difficult; the real challenge is how to make users willing to continue reading after finishing one article.
Especially since many users do not enter from the blog homepage, but rather “accidentally” land on a specific article through search engines, reposted links, or even AI search summaries. At this point, if the article itself lacks clear structural relationships and reading paths, users will often close the page directly after reading the current content, thereby achieving the “100% bounce rate” achievement.
For series articles, this is actually a great pity. Because series articles naturally possess strong contextual relationships: the previous article sets the stage, and the next article extends it. The articles are not independent “information fragments” from each other, but rather a continuously unfolding knowledge path.
However, traditional blog systems do not support this “continuous reading relationship” very well. Although WordPress itself has a “Previous / Next” function, it is mostly based on the chronological order of publication rather than true content structural relationships.
As for some off-the-shelf “series article” plugins, although they can provide features like ”series article navigation” for such articles, they usually introduce extra database structures, backend management logic, and relatively heavy plugin dependencies. This brings us back to the issue I mentioned in a previous article (see:Postscript: Why I Didn't Choose Off-the-Shelf “Related Articles” Plugins), and I don't really want my blog to become increasingly dependent on “functional plugins” to achieve these structural capabilities.
Therefore, this idea of “series article navigation” has actually always existed, but in the past, I could never find an implementation method that was lightweight and natural enough, so I put it on hold.
Until now, with the emergence of “article-index.json”, I suddenly realized that many features that previously had to rely on database queries, plugin systems, or even complex backend configurations can now be easily implemented at WordPress runtime through a lightweight index file. “Series article navigation” is a very typical example of this.
2 The Difference Between “Series Relationships” and “Semantic Relationships”
In the previous article (see:Building a “Lightweight Knowledge Index" for My Blog (Part 3): Design and Implementation of a Lightweight WordPress Recommendation System Based on Pre-computed Semantic Indexing), we have already implemented the “related articles” feature in the blog based on article-index.json.
However, when I actually started researching the “series article navigation” feature, I quickly discovered that although both it and the “related articles” feature ultimately manifest as “associations between articles,” the types of relationships behind them are actually different.
For the current “related articles” feature, it essentially still belongs to a “strong association relationship.” That is to say, these association relationships are not randomly generated, but are actively written into article-index.json after preprocessing, for example:
“14257”: {
“title”: “xxx”,
“related”: [
“14244”,
“14228”,
“13534”
]
}
“title”: “xxx”,
“related”: [
“14244”,
“14228”,
“13534”
- ]
- }
The related here essentially belongs to a kind of "manually curated content relationship." It is more like: "After reading the current article, what content is suitable to continue reading." Therefore, although it is a type of "article relationship," it does not possess a strict sequential structure.
- On the other hand, "series article navigation" is completely different, because "series relationships" essentially belong to a type of "structural relationship (Structure Edge)." A so-called structural relationship means: there is a clear sequential order between articles; there is a continuous reading path; and the current article has a definite position within the entire series.
- For example: "Awakening of Voice (Part 1)", "Awakening of Voice (Part 2)", "Awakening of Voice (Part 3)". The relationship here is not "these articles are related to each other," but rather "these articles themselves are continuous nodes on the same reading path." This relationship is actually more definite than ordinary "related articles": because "related articles" are more about content extension, reading recommendations, and related topics; whereas "series relationships" are about reading order, structural positioning, and contextual continuation. The two solve different problems.
- Because of this, I quickly realized that "series article navigation" does not actually require building a new, complex system. It requires neither additional database structures nor complex backend configurations. What is truly needed is simply: which series the current article belongs to, its current position in the series, and what other articles are in the same series.
- And this information can actually be obtained directly based on article-index.json. Because article-index.json itself already plays the role of the "unified index layer for blog articles." As long as we can identify through title rules:
- Series name (series_key)
Current sequence number (series_index)
Then the remaining problem actually becomes very simple:
3 Series Identification Mechanism Based on Title Rules
Traverse article-index.json
Find articles with the same series_key
Sort by sequence number
Locate the position of the current article
Get the previous and next articles
Therefore, in terms of implementation, a certain normalization process needs to be applied to the titles. Through simple regular expression rules, different forms of numbering are unified into the same numerical structure, such as mapping “one, two, three” and “1, 2, 3” to integer sequence numbers. This process is not complex, but its significance lies in allowing “human writing habits” to be stably mapped to a “system-computable structure.” After completing this layer of parsing, the next step is to truly enter the data layer processing, which is to complete global matching with the help of article-index.json.
The role of article-index.json here is not to store relationships, but to exist as a lightweight article index table. It records the basic information of all articles in the blog, including titles, URLs, and article IDs. This allows the system to traverse all articles at runtime and then execute the same series identification logic for each article.
Once the identification is complete, you only need to filter out articles with the same series_key and then sort them by series_index, and a complete series structure is naturally formed.
At this point, series identification no longer relies on any complex external systems, but is entirely completed by “title rules + lightweight index.” Its essence is actually to make the structural relationships originally hidden in writing habits explicit, and to allow the system to stably reconstruct this relationship at runtime.
After this layer is done, the subsequent steps become very straightforward: how to use this already structured series result in PHP to calculate the position of the current article and generate the navigation relationships for the previous and next articles.
4 Rendering and PHP Implementation of Series Article Navigation
On the UI, I did not design the series navigation as a complex list, but tried to keep it lightweight, retaining only two core pieces of information: the series name, current progress (e.g., 4 / 8), and the chapter index of the entire series, with a display effect similar to:
📚 系列文章:声音的觉醒(4 / 8)
1 2 3 4 5 6 7 8
1 2 3 4 5 6 7 8
The reason for doing this is simple: the core value of series navigation is no longer just a “path to continue reading,” but to allow readers to see the structure of the entire series at a glance, making it easy to freely choose chapters of interest while keeping the overall interface clean and the reading rhythm steady.
In terms of WordPress implementation, this part is done through the_content filter to complete the insertion. Specifically, it dynamically splices an HTML fragment during the content output process and then inserts it into the post body.
There is a key but easily overlooked detail here: the design of the insertion position order. In the current blog structure, in addition to the series navigation, there is also a “content structure prompt” module, which is used to guide users into the blog's knowledge map system:

From a purely technical perspective, both modules could simply be appended to the end of the article, but from a reading experience perspective, this order is actually not ideal.
The final design is: the series article navigation appears before the content structure prompt. That is to say, after the main text of the article, the user first sees the “continuous reading path of the current series” (the position is shown in the red box below), and then the “content structure prompt”:

This order is not arbitrary, but a very clear choice of reading priority: series navigation solves the problem of “continuing to read the same linear content,” while the structure prompt solves the problem of “jumping out of the current content system and entering the global knowledge structure.”
Providing a continuous path first while the user is still in the reading state is more in line with natural reading behavior; providing a structured entry after the user has finished reading is more like an “exit navigation supplement.”
Therefore, in terms of implementation, by controlling the_content the filter priority of, as well as the HTML splicing order, it ultimately ensures that the Series Card always appears before the content structure prompt, thereby forming a transition from “local continuity” to “global structure.”
Based on the ideas in Chapter 3, the PHP implementation is as follows (same as the previous article,article-index.json located in the theme directory:wp-content/themes/argon-theme-master/cache/article-index.json):
function extract_series_info(title) {title = trim(str_replace(' ', ' ', title));
if (preg_match('/^(.*?)[((]([一二三四五六七八九十百零]+)[))]/u',title, matches)) {series_key = trim(matches[1]);map = [
'一' => 1, '二' => 2, '三' => 3, '四' => 4, '五' => 5,
'六' => 6, '七' => 7, '八' => 8, '九' => 9, '十' => 10
];
index_cn =matches[2];
index = isset(map[index_cn]) ?map[index_cn] : 0;
if (index > 0) {
return [
'series_key' => series_key,
'series_index' =>index
];
}
}
if (preg_match('/^(.*?)(?:Part\s*|part\s*|\()(\d+)\)?/u', title,matches)) {
return [
'series_key' => trim(matches[1]),
'series_index' => intval(matches[2])
];
}
return null;
}
function add_series_card_after_content(content) {
if (!is_single() || !in_the_loop() || !is_main_query()) {
returncontent;
}
if (get_post_type() !== 'post') {
return content;
}current_id = get_the_ID();
json_path = get_template_directory() . '/cache/article-index.json';
if (!file_exists(json_path)) {
return content;
}articles = json_decode(file_get_contents(json_path), true);
if (!articles || !isset(articles[current_id])) {
return content;
}current_title = articles[current_id]['title'];
current_series = extract_series_info(current_title);
if (!current_series) {
returncontent;
}
series_key =current_series['series_key'];
series_articles = [];
foreach (articles as id =>article) {
if (empty(article['title'])) continue;series_info = extract_series_info(article['title']);
if (!series_info) continue;
if (series_info['series_key'] ===series_key) {
series_articles[] = [
'id' =>id,
'url' => article['url'],
'index' =>series_info['series_index']
];
}
}
if (count(series_articles)<2) {
returncontent;
}
usort(series_articles, function(a, b) {
returna['index'] <=> b['index'];
});total = count(series_articles);current_index = -1;
foreach (series_articles asi => article) {
if ((string)article['id'] === (string)current_id) {current_index = i;
break;
}
}
if (current_index === -1) {
return content;
}
// =========================
// UI
// =========================html = '<div class="series-card">
<div class="series-card-inner">
<div class="series-title">
📚 系列文章:' . esc_html(series_key) . '(' . (current_index + 1) . ' / ' . total . ')
</div>
<div class="series-numbers">';
foreach (series_articles as i =>article) {
num =i + 1;
if (i ===current_index) {
html .= '<span>' .num . '</span>';
} else {
html .= '<a href="' . esc_url(article['url']) . '">' . num . '</a>';
}
}html .= '</div>
</div>
</div>';
return content .html;
}
add_filter('the_content', 'add_series_card_after_content', 5);
Add the following additional CSS in the active theme:
/* =========================
Series Card 容器
========================= */
.series-card {
display: flex;
justify-content: center;
margin: 14px 0;
}
.series-card-inner {
display: inline-flex;
flex-direction: column;
align-items: center;
padding: 10px 12px;
background: #f8fafc;
border: 1px solid #e5e7eb;
border-radius: 8px;
max-width: 760px; /* 关键:稳定不换行 */
}
/* =========================
标题行
========================= */
.series-title {
font-size: 13px;
font-weight: 600;
color: #111827;
margin-bottom: 6px;
line-height: 1.2;
white-space: nowrap;
}
/* =========================
数字行
========================= */
.series-numbers {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: nowrap; /* 禁止换行 */
gap: 6px;
line-height: 1;
}
/* 普通数字 */
.series-numbers a {
color: #0369a1;
text-decoration: none;
font-size: 13px;
font-weight: 500;
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 6px;
}
/* 当前数字 */
.series-numbers span {
color: #dc2626;
font-size: 13px;
font-weight: 700;
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 6px;
}
Series Card Container

========================= */
5 A Lightweight but Complete Reading Structure Patch
.series-card {
display: flex;
justify-content: center;
margin: 14px 0;
}
.series-card-inner {
display: inline-flex;
padding: 10px 12px;
background: #f8fafc;
Chrome 148.0.0.0