Building a Lightweight Knowledge Index for Blogs (Part 7): Right-hand Menu – A Unified Organization Mechanism for Semantic Space and Classification System
Article Summary
为解决博客推荐内容触达率低的问题,设计了一种基于语义相似性与分类体系的右侧悬浮菜单方案。通过离线生成文章语义向量与分类语义空间,结合WordPress前端动态渲染,在不干扰阅读体验的显眼位置实现推荐内容的持续曝光。该方案利用embedding-index.json与category-space.json分层数据结构,将分类从静态标签升级为语义聚合体,使推荐逻辑从“文章→文章”扩展为“文章→分类→文章”的结构化路径,最终形成更立体的知识网络,提升用户探索深度与内容关联性。
Qwen3-14B · 2026-07-01

1 Introduction

In previous articles in this series, I added a "Related Articles" section at the bottom of the article page (see:Building a Lightweight Knowledge Index for Blogs (Part 3): Design and Implementation of a Lightweight Recommendation System for WordPress Based on Pre-computed Semantic IndexesThe "Guess What You Want" and "Guess What You Think" features are used to recommend content based on different dimensions after a visitor has finished reading a single article.

image.png

Among them, "related articles" are more inclined to structured relationships, such as content continuation under the same topic or upstream and downstream connections; while "what you might want" relies more on semantic similarity, using embedding vector calculation to find articles that are close in semantic space.

The original intention of this design was to make the blog system more than just a "collection of content," but to establish horizontal connections between articles, allowing knowledge to unfold in a network-like manner, thereby improving overall explorability and reducing user bounce rate.

However, through actual observation, I gradually realized a problem: relying solely on the recommendation mechanism at the end of the article has limited coverage. In reality, many visitors don't read an entire article, let alone scroll to the bottom of the page to view recommended content. Therefore, these carefully designed related recommendations actually have a low reach rate, and in many access scenarios, they are more like a structure that "exists but is not used."

Based on this question, I began to rethink the display hierarchy of recommendation systems: Is it necessary to rely on the end of the article? Is there a more frequently used, more prominent position that won't interfere with the reading experience?

While observing the page layout, I noticed the blank area on the right side of the article page—especially after disabling the sidebar tools in the Argon theme, this area was almost completely unused:

image.png

However, it is also located at the natural boundary of the user's reading field of vision, making it very suitable for carrying some "auxiliary information".

So I tried to introduce a floating related recommendation component in this position. It no longer waits for the user to finish reading the article, but exists continuously in the reading process as a "knowledge entry point that is always accessible".

So, what logic should this entry point be based on to organize its content? At this point, I returned to a core structure that already exists in the blog:Blog Knowledge MapThe classification system within the text is not simply a set of labels, but rather a structured aggregation of topics, essentially representing a higher level of knowledge organization. When combined with the semantic similarity between articles, a dual filtering mechanism of "classification × semantic space" can be formed.

Therefore, this right-side floating menu was ultimately defined as "a dynamic association recommendation layer that aggregates and sorts articles within an existing category structure based on semantic similarity." Its function is no longer simply "recommending the next article," but rather providing a more structured entry point: allowing users to start from their current reading content and horizontally expand their reading path across category dimensions.

In a sense, this layer of structure makes up for the shortcomings of the recommendation mechanism that originally only existed at the bottom of the article in terms of "exposure position", making the content network of the entire blog more three-dimensional and closer to an explorable knowledge system, rather than a linear list of articles.

2 Design

2.1 Design concept of the right-side menu

Actually, the initial goal was not the right-hand menu. The earliest goal was to create an in-site semantic search system based on semantic vectors—so that the relationship between articles no longer depends on traditional tags or categories, but directly on the distance in the embedding space to determine the recommendation relationship.

However, during the actual implementation, a key constraint was quickly encountered: the model used to generate the embeddings (qwen3-embedding:8b) was deployed in an intranet environment and was not suitable for direct exposure to public services. Insisting on "real-time semantic search" meant bringing the entire vector generation capability online, which wasn't a good option under the current deployment structure—even if published to the public internet via a Cloudflare tunnel, access speeds within China wouldn't be significantly faster. Of course, using a paid cloud API was an option, but I wasn't willing to spend the money.

This limitation, in turn, prompted me to rethink "where semantic capabilities should be placed"—since real-time computation is not possible, we can only take a step back and move semantic computation to the offline stage, that is, calculate and solidify all the vectors of the article in advance, and then only perform lightweight similarity calculation and aggregation when the front-end makes a request.

In fact, when implementing the "Guess What You Think" feature, I already had an embedding-based implementation and generated a semantic-vector.json file. However, that data structure was more geared towards single-article-level semantic indexing, with a flat "article → vector" structure. This exposed some problems when used for cross-category aggregation, such as inconsistent data granularity and the need for additional structural reorganization when expanding category dimensions. Therefore, at this stage, I chose not to directly reuse the old structure, but instead reconstructed an article-level system, "embedding-index," oriented towards "category relationship calculation," leaving room for subsequent category-level semantic modeling.

With article-level embeddings in place, the next step becomes quite natural: since similarity between articles can be calculated using vectors, categories can be viewed as "semantic aggregates of article sets." Specifically, each category is no longer just a static label, but rather a "semantic center vector" that can be calculated from the vectors of its representative articles. When a user visits an article, the similarity between the current article vector and the vectors of each category can be calculated first, thus obtaining the set of top categories that are semantically closest.

After identifying the most relevant categories (after comprehensive consideration, three categories were deemed most suitable), further ranking by article-level similarity within these categories (four articles were tentatively chosen as the most suitable) yields a hierarchical recommendation result: first, the "Top 3 categories most relevant to the current article," and then the "Top 4 semantically closest articles" within each category. In this way, the recommendation is no longer just a simple matching of individual articles, but forms a two-level semantic structure from "article → category → article."

Overall, the right-hand menu is not a simple navigation component, but rather it explicitly exposes the "semantic organization structure" that was originally hidden in the background to the user interface, making categorization no longer just an information archiving tool, but a dynamic dimension of content association.

2.2 Data Structures and System Inputs: The Semantic Organization Foundation Based on Three Types of JSON

After clarifying the design concept of the right-side menu, the system implementation actually relies on a set of data structures generated layer by layer. These structures do not exist in isolation, but are built step by step along the path of "content generation → semantic expression → structural aggregation", ultimately forming the complete data foundation of the right-side menu.

Overall, this system mainly relies on three core JSON files, which are at different semantic levels and have clear generation dependencies.

1. embedding-index.json

First, let's look at the data source for the article's semantic layer, corresponding to `embedding-index.json`. This layer's data isn't directly generated from the article itself, but rather built upon the JSON file output from the previous article on "AI summarization" (see:Building a Lightweight Knowledge Index for Blogs (Part 6): AI Summarization and Editable Semantic Layer DesignSpecifically, it depends on two intermediate files: summary-source.json and summary-index.json.

The `summary-source.json` file provides the raw semantic input of the article content, while `summary-index.json` performs structured organization and summarization on this basis. `embedding-index.json` further generates vector representations on top of this semantic compression result, ensuring that each article enters a unified semantic space.

From a data structure perspective, a typical embedding-index.json file has the following structure:

{ "14334": { "post_id": "14334", "url": "/technology/homedatacenter14334/", "title": "Building a Lightweight Knowledge Index for Blogs (Part 4): Navigation and Reading Path Design for Series Articles", "embedding": [0.008560282, -0.02900768, 0.013086175, ...], "control": { "roadmap_hint": null }, "meta": { "summary": "Addressing the issue of insufficient continuity in reading blog series articles, a structured navigation solution is implemented by building a lightweight knowledge index. Based on the article-index.json file and title rules, series identifiers and sequence numbers are extracted without adding any database structure or plugin dependencies...", "updated": "2026-06-10 10:58:56", "model": "qwen3-embedding:8b", "content_hash": "df3b496a82d707594fd3b3433f70aebd1352d286303c1c9742b2a5e7e1e5fc42" } } }

As can be seen, this structure not only contains the vectors themselves, but also retains the basic meta-information and summary results of the article. This means that embedding-index.json is not a "pure vector file", but a hybrid index layer that integrates semantic representation and content context.


Note: From an engineering implementation perspective, there is a key design choice at this layer: whether to "reuse" existing intermediate products.

In the current implementation, embedding-index.json reuses summary-source.json and summary-index.json, which were already generated by the AI summarization system, to some extent. The advantage of this approach is that it can significantly reduce computational complexity and reduce the cost of repeatedly parsing the original WordPress content.

However, it's important to emphasize that this "reuse of existing resources" approach is not the only solution. Theoretically, it's entirely possible to bypass the existing middleware layer and rebuild the embedding system directly from the original WordPress post content. However, this would significantly increase the complexity of data cleaning, text normalization, and semantic segmentation, while also lengthening the generation process and placing higher demands on script stability and maintenance costs.

Therefore, the current design chooses a compromise: prioritize the reuse of existing semantic intermediate products, reduce the overall system complexity while ensuring consistency, and make embedding-index.json a stable and reconstructable semantic layer that does not need to be built from scratch every time.


2. roadmap-source.json

Above the semantic layer of the article is the knowledge map structure layer, corresponding to roadmap-source.json.

The data in this layer comes from the blog knowledge map itself. It records the structural information of all categories in the system, as well as the list of articles contained in each category.

From a data structure perspective, its core form is as follows:

{ "categories": [ { "id": "infra", "name": "Personal Digital Infrastructure and Blog System Construction", "articles": [ { "title": "A Brief Discussion on How to Choose the Most Suitable NAS for You", "url": "/technology/nas602/" }, { "title": "Synology NAS HDD Data Drive Automatic Hibernation", "url": "/technology/nas139/" }, { "title": "A Complete Guide to Building a Personal Blog from Scratch (Minimum Cost)", "url": "/technology/homedatacenter12540/" } ] } ] }

Unlike the embedding layer, this layer does not involve any semantic computation. Its role is to provide a stable structural boundary, ensuring that semantic recommendations do not deviate from the established content organization system. In other words, it defines "how blog content is organized," rather than "how content is semantically related."

From a system design perspective, this layer is more like a "human-built knowledge skeleton". All semantic calculations must be completed within this skeleton to ensure that the recommendation results do not deviate from the original knowledge structure of the blog.

3. category-space.json

The third layer is the category semantic space layer, corresponding to category-space.json.

The data in this layer is the most critical fusion product in the entire system, and it depends on both roadmap-source.json and embedding-index.json.

Structurally, its core form is as follows:

{ "categories": [ { "id": "infra", "name": "Personal Digital Infrastructure and Blog System Construction", "embedding": [ 0.0108209606, -0.0248727239, 0.0033332910, -0.0051341676 ], "articles": [ { "title": "A Brief Discussion on How to Choose the Most Suitable NAS for You", "url": "/technology/nas602" }, { "title": "Synology NAS HDD Data Drive Automatic Hibernation", "url": "/technology/nas139" }, { "title": "A Complete Guide to Building a Personal Blog from Scratch (Minimum Cost)", "url": "/technology/homedatacenter12540" } ] } ] }

The generation logic can be understood as follows: first, obtain the collection of articles under each category based on roadmap-source.json, then extract the vector representation of these articles from embedding-index.json, and finally aggregate and calculate all article vectors within the same category to obtain the semantic center vector of that category.

In this structure, each category is no longer just a static directory, but a "semantic aggregate" determined by the semantics of multiple articles. The embedding field is a direct expression of this aggregation result, which upgrades the category from a "structural unit" to a "semantic unit".

At the same time, each category will retain a number of representative articles (articles field) to display specific clickable content on the front end, so as to maintain a direct mapping relationship between the semantic layer and the readable content.

Ultimately, the recommendation logic of the right-hand menu is built on this layer: by calculating the similarity between the current article vector and the vectors of each category, the most relevant category is selected, and then specific articles are selected within each category.

2.3 System Overall Architecture and Engineering Value

From an overall structural perspective, these three types of JSON are not parallel but rather form a clear hierarchical dependency system:

summary-source.json ↓ summary-index.json ↓ embedding-index.json ↓ ├───────────────┐ ↓ ↓ roadmap-source.json (Knowledge Map Structure) ↓ ↓ └──────┬────────┘ ↓ category-space.json ↓ Right-side menu recommendation system

From an engineering perspective, the core value of this design lies in the fact that it moves the semantic computation and structural aggregation process, which originally needed to be completed at runtime, to the offline construction stage and connects them through a set of JSON intermediate products.

In this process, each layer of JSON performs only a single responsibility: either semantic expression, structural organization, or final aggregation output, without introducing additional computational burden at runtime. Ultimately, the WordPress side only needs to consume category-space.json to complete the presentation of the recommendation logic.

Therefore, the essence of this structure is not a "multi-stage data processing flow", but an engineering convergence strategy for CMS scenarios: concentrating the complexity in the construction stage (the core idea of the entire series of articles), solidifying the deterministic output into static data, and enabling semantic recommendation capabilities to be embedded into traditional content systems in a low-cost manner.

3. Project Implementation: From Offline Building to Front-End Rendering

3.1 Overall Process of Project Implementation

Based on the design concept in Chapter 2, in actual engineering implementation, this right-side menu system can be broken down into three distinct steps, each with its corresponding output and execution location.

The first step is to generate an article-level semantic vector index file. embedding-index.jsonThis part is written in script. build_embedding_index.py This file is responsible for converting WordPress post content into a unified semantic vector representation and persistently storing it in JSON format. It can be understood as the "semantic foundation" of the entire system; all subsequent recommendation calculations rely on the post-level embedding data it provides.

The second step is to generate a classification-level semantic space file. category-space.jsonThe corresponding script is build_category_space.pyThe input at this stage not only includes the article vector data generated in the first step, but also incorporates the classification structure information from the blog knowledge map. It aggregates and calculates article vectors within the same category to generate a semantic center vector for each category, while retaining a list of representative articles within that category. This file essentially builds a "classification semantic space" on top of the article semantic space.

The third step is to implement the actual display logic on the WordPress front-end. The system will read... category-space.json The system processes the data and, combined with the current article information, calculates the semantic similarity between the data and each category. Finally, it selects the most relevant categories and renders the corresponding list of recommended articles in the right-hand menu. In other words, this step does not perform semantic calculations; it simply consumes and displays existing structured data.

The advantage of this splitting approach is that each layer has a clear boundary of responsibility: semantic computation is completed offline, and the front end is only responsible for consuming the results, thus avoiding the introduction of complex computational logic into the WordPress runtime.

3.2 Article semantic vector generation: build_embedding_index.py

The code for the build_embedding_index.py script is as follows:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import json
import os
import time
import requests
import hashlib

# -----------------------------
# Ollama embedding 服务(需根据实际场景修改)
# -----------------------------
OLLAMA_URL = "http://127.0.0.1:11434/api/embed"
MODEL_NAME = "qwen3-embedding:8b"

# -----------------------------
# 路径配置(需根据实际场景修改)
# -----------------------------
CACHE_DIR = "/docker/wordpress/html/wp-content/themes/argon-theme-master/cache/"
SUMMARY_FILE = os.path.join(CACHE_DIR, "summary-index.json")

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
SOURCE_FILE = os.path.join(SCRIPT_DIR, "summary-source.json")
OUTPUT_FILE = os.path.join(CACHE_DIR, "embedding-index.json")

# -----------------------------
# 工具函数
# -----------------------------
def load_json(path, default):
    if not os.path.exists(path):
        return default
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)


def make_hash(text):
    return hashlib.sha256(text.encode("utf-8")).hexdigest()


# -----------------------------
# embedding 请求
# -----------------------------
def get_embedding(text, retries=3):
    payload = {
        "model": MODEL_NAME,
        "input": text
    }

    for i in range(retries):
        try:
            resp = requests.post(
                OLLAMA_URL,
                json=payload,
                timeout=180
            )

            if resp.status_code == 200:
                data = resp.json()
                emb = data.get("embeddings", [])
                if emb:
                    return emb[0]

        except Exception as e:
            pass

        time.sleep(2 * (i + 1))

    return None


# -----------------------------
# 轻量规则(预留结构信息)
# -----------------------------
def infer_roadmap_hint(text):
    t = text.lower()

    if "cloudflare" in t:
        return "cloudflaremap"
    if "embedding" in t or "ai" in t or "llm" in t:
        return "aimap"
    if "music" in t or "sound" in t:
        return "singmap"

    return None


# -----------------------------
# 主流程
# -----------------------------
def main():
    source = load_json(SOURCE_FILE, [])
    summary = load_json(SUMMARY_FILE, {})
    cache = load_json(OUTPUT_FILE, {})

    for article in source:
        post_id = str(article.get("id"))
        if not post_id or post_id in cache:
            continue

        summary_item = summary.get(post_id)
        if not summary_item:
            continue

        title = summary_item.get("title", "")
        summary_text = summary_item.get("summary", "")
        url = article.get("url", "")

        embedding_input = title + "\n" + summary_text
        embedding = get_embedding(embedding_input)

        if not embedding:
            continue

        cache[post_id] = {
            "post_id": post_id,
            "url": url,
            "title": title,
            "embedding": embedding,
            "control": {
                "roadmap_hint": infer_roadmap_hint(title + " " + summary_text)
            },
            "meta": {
                "summary": summary_text,
                "model": MODEL_NAME
            }
        }

        with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
            json.dump(cache, f, ensure_ascii=False, separators=(',', ':'))


if __name__ == "__main__":
    main()

Functionally, this script mainly accomplishes three things:Read article summary data, call the embedding model to generate semantic vectors, and output a JSON index file with a unified structure..

The most crucial part isn't the embedding call itself, but rather the unification process of the entire data structure. At this stage, the system no longer directly processes the original WordPress post content, but rather uses the data generated in the previous stage. summary-source.json and summary-index.jsonThe article title and abstract are concatenated as semantic input to generate a stable vector representation.

Final output embedding-index.jsonEach record contains the following core fields:

  • post_idArticle unique identifier
  • urlArticle link path
  • titleArticle title
  • embeddingArticle semantic vector (core field)
  • control.roadmap_hintLightweight structure hints (for weak classification assistance)
  • meta.summaryArticle summary information
  • meta.model: Identifier of the embedding model used

From a systems perspective, the significance of this step is:The text information that was originally scattered throughout WordPress and the summary system is uniformly transformed into computable semantic space data.All subsequent category aggregation and recommendation logic is built directly on this embedding index.

3.3 Categorical semantic space generation: build_category_space.py

After obtaining the article-level semantic vectors, the next step is to use the `build_category_space.py` script to further converge these "point-like article semantics" into a "category-level semantic space," which is ultimately used for the right-hand menu recommendations. category-space.jsonThe script code is as follows:

#!/usr/bin/env python3 # -*- coding: utf-8 -*- import json import numpy as np from sklearn.metrics.pairwise import cosine_similarity # =========================== # Config # =========================== ROADMAP_FILE = "roadmap-source.json" CACHE_DIR = "/docker/wordpress/html/wp-content/themes/argon-theme-master/cache/" EMBEDDING_FILE = CACHE_DIR + "embedding-index.json" OUTPUT_FILE = CACHE_DIR + "category-space.json" MODE = "hybrid" # struct | semantic | hybrid ALPHA = 0.4 # struct weight # ========================= # Utils # ========================= def load_json(path): with open(path, "r", encoding="utf-8") as f: return json.load(f) def l2_normalize(vec): arr = np.array(vec, dtype=np.float32) if arr.ndim == 1: norm = np.linalg.norm(arr) return arr / (norm + 1e-10) norm = np.linalg.norm(arr, axis=1, keepdims=True) return arr / (norm + 1e-10) def normalize_url(url: str): if not url: return url return url.rstrip("/") def struct_weight(title: str): w = 1.0 if any(x in title for x in ["(一)", "(二)", "(三)", "(四)", "Part", "Series"]): w += 0.2 if any(x in title for x in ["Architecture", "System", "Design", "Reconstruction", "Program"]): w += 0.3 if len(title) < 18: w -= 0.2 return max(w, 0.5) def build_embedding_map(embedding_data): emb_map = {} for post_id, item in embedding_data.items(): if not isinstance(item, dict): continue url = normalize_url(item.get("url")) embedding = item.get("embedding") if not url or not embedding: continue emb_map[url] = l2_normalize(embedding) return emb_map def build_category_groups(roadmap): groups = {} for cat in roadmap["categories"]: groups[cat["id"]] = { "id": cat["id"], "name": cat["name"], "articles": cat["articles"] } return groups def semantic_weights(embeddings): if len(embeddings) == 0: return [] embs = np.array(embeddings) if len(embs) == 1: return [1.0] mean = np.mean(embs, axis=0).reshape(1, -1) sims = cosine_similarity(embs, mean).flatten() denom = sims.max() - sims.min() if denom < 1e-10: return [1.0] * len(sims) sims = (sims - sims.min()) / denom return 0.5 + sims def compute_centroid(embeddings, weights): if len(embeddings) == 0: return None embeddings = np.array(embeddings) weights = np.array(weights).reshape(-1, 1) centroid = np.sum(embeddings * weights, axis=0) / (np.sum(weights) + 1e-10) return l2_normalize(centroid).tolist() def build(): roadmap = load_json(ROADMAP_FILE) embedding_data = load_json(EMBEDDING_FILE) emb_map = build_embedding_map(embedding_data) categories = build_category_groups(roadmap) output = {"categories": []} for cid, cat in categories.items(): embs = [] struct_ws = [] articles_out = [] for a in cat["articles"]: url = normalize_url(a["url"]) if url not in emb_map: continue emb = emb_map[url] embs.append(emb) struct_ws.append(struct_weight(a["title"])) articles_out.append({ "title": a["title"], "url": url }) if len(embs) == 0: continue embs_np = np.array(embs) sem_ws = semantic_weights(embs_np) n = len(embs) struct_ws = struct_ws[:n] sem_ws = sem_ws[:n] if MODE == "struct": final_ws = struct_ws elif MODE == "semantic": final_ws = sem_ws else: final_ws = [ ALPHA * sem_ws[i] + (1 - ALPHA) * struct_ws[i] for i in range(n) ] centroid = compute_centroid(embs_np, final_ws) output["categories"].append({ "id": cid, "name": cat["name"], "embedding": centroid, "articles": articles_out }) with open(OUTPUT_FILE, "w", encoding="utf-8") as f: json.dump(output, f, ensure_ascii=False, indent=2) if __name__ == "__main__": build()

Functionally, this script can be summarized in one sentence:Compress the "article semantic vector" into a "classification semantic vector".“Its input comes from two parts: one part is generated in the previous section. embedding-index.jsonOne part stores the semantic vector of each article; the other part is... roadmap-source.jsonThis is a blog knowledge map, which provides a category structure and a list of articles under each category.

During the processing, the script will first... embedding-index.json This process converts the text into a vector map indexed by the URL. The purpose of this step is to allow articles within a category to quickly find their corresponding semantic vectors. Meanwhile,roadmap-source.json It will be parsed into categorical structure data to determine which articles are included in each category.

Next, the system will aggregate the article vectors within each category. This is not a simple averaging, but rather a weighted mechanism is introduced: it considers both semantic similarity (semantic weight) and article structural features (struct weight), such as whether it belongs to a series of articles, whether it is more related to architectural design, etc.

Ultimately, each category is compressed into a "semantic center vector" while retaining the list of representative articles within that category; this is the final output. category-space.json The core content.

In the end, after this step was completed, the “categories” in the system were no longer just the directory structure in WordPress, but became a semantic entity that could participate in similarity calculation, providing a foundation for the recommendation logic of the subsequent right-side menu.

3.4 WordPress Right-Side Menu Implementation: Front-End Rendering and Semantic Recommendation

3.4.1 Front-end architecture breakdown: division of responsibilities for PHP / JS / CSS

This part is the truly "visible" endpoint of the entire system, which is how the right-side floating menu is actually presented on the WordPress page. Compared to the previous chapters which focused on data structures and engineering generation, the focus of this part has shifted from "how to calculate semantics" to "how to stably and cost-effectively present the generated semantic results on the front end".

From an implementation perspective, this module is divided into three parts: PHP (server-side computation and data organization layer), JavaScript (front-end interaction and state control layer), and CSS (visual presentation layer). This division is not simply determined by WordPress's operating model, but rather naturally formed by the system's own boundaries of responsibility—that is, the need to separate "deterministic computation" from "interactive state management" while maintaining the lightweight and stateless characteristics of the front-end.

By using this decomposition method, the system logically forms a clear three-layer responsibility boundary, which allows recommendation calculation, interaction control and visual presentation to be decoupled from each other, thereby reducing the overall system complexity and maintenance costs.


In this structure, the PHP part serves as both a "data entry point" and an "online recommendation calculation layer." It is responsible for injecting the HTML container for the right-hand menu into the article page and loading... category-space.json and embedding-index.json The data files generated offline are used, and on the server side, similarity calculation within the category, candidate article ranking, and application of filtering rules (such as self-similarity filtering and series deduplication) are completed based on the embedding vector of the current article.

This can be understood as follows: PHP at this layer does not simply move data, but rather undertakes the responsibility of "online deterministic recommendation computation based on pre-computed semantic space", and finally passes the computation results to the front-end JavaScript.

The JavaScript component serves as the interaction and display control layer for the entire right-hand menu. It is responsible for reading the structured data objects injected into the page by PHP. window.SIDEBAR_DATAThis data not only includes category-space.json The classification structure also includes the final recommendation results calculated by PHP at runtime based on the current article embedding.

At the rendering level, JavaScript dynamically generates and inserts the category list and corresponding article list into the DOM, thus constructing the complete UI structure of the right-hand menu. At the interaction level, it controls the state transition between category switching and article panel display by listening to mouse hover and enter/leave actions, implementing a lightweight UI state machine. It is important to note that this layer does not participate in any similarity calculation or sorting logic; JavaScript's responsibility is limited to transforming the "calculated recommendation structure" into an interactive user interface.

The CSS portion focuses entirely on the visual presentation layer and does not participate in any logical processing. It is mainly responsible for the layout structure, hierarchy, animation effects, and visual feedback of the right-side floating menu in the hover state, ensuring that the component has sufficient visibility on the page without interfering with the reading experience of the main text.


Through this three-layer breakdown, the entire right-side menu system in WordPress is built into a clear hierarchical structure: PHP is responsible for online similarity calculation based on embedding, sorting within categories, application of filtering rules, and organization and output of the final recommendation results; JavaScript is responsible for transforming the structured recommendation results output by PHP into interactive UI state transitions on the front end; and CSS is responsible for the final visual presentation and interactive feedback.

The core value of this design lies in its complete centralization of semantic recommendation computation to the server-side execution, freeing the front-end from any computational or decision-making logic and thus maintaining the lightweight and deterministic nature of the UI layer. Simultaneously, the recommendation logic no longer relies on WordPress's rendering lifecycle but exists as an independent computation module, interacting with the front-end solely through structured data.

From a system perspective, this separation is not a simple front-end and back-end separation, but a lightweight semantic architecture that decouples computation and expression, allowing recommendation capabilities to evolve independently without affecting the stability of the content system itself.

3.4.2 WordPress Side Data Construction and Recommendation Logic Implementation (PHP)

This part is the server-side calculation and data assembly layer of the right-hand menu in WordPress. Its responsibility is not only to take over the basic data such as embedding and category-space generated offline, but more importantly, to perform similarity calculation based on the current article vector during the page request stage, and to complete the generation of sorting, filtering and recommendation results within the category.

In other words, this layer is responsible for both "the carrying and organization of the offline semantic space" and "online recommendation calculation based on the current context," ultimately encapsulating the calculation results into structured data.SIDEBAR_DATAThis is for use by front-end JavaScript.

From an overall structural perspective, this PHP code mainly does three key things: unifies the data access path, builds the semantic baseline of the current article, and generates the final category recommendation results for front-end display.

First, during the data preparation phase, the system loads data from the cache directory. embedding-index.json and category-space.jsonThe former provides a vector representation of each article, while the latter provides a classification space and its set of articles. During loading, PHP normalizes the article URLs to ensure that data from different sources can be matched within the same indexing system.

Building upon this foundation, the system constructs two key mapping relationships: one is a mapping from "article URL to embedding vector," used for subsequent similarity calculations; the other is a mapping from "article URL to series information," derived from the title parsing logic in the article index data, used to identify the structural relationships between articles within the same series. The significance of this design layer lies in its consideration of not only semantic similarity but also the introduction of structural constraints to prevent the repetition of content from the same series in recommendations.

When retrieving the vector of the current article, the system first attempts to read it directly from the embedding mapping. If there is no corresponding vector for the current article (e.g., due to missing index or anomalies), it will degenerate into using the global vector mean as a substitute benchmark, thus ensuring that the recommendation process can always run without system interruption due to a single point of missing data.

Next, we move on to the core recommendation calculation stage. The system iterates through the article collection under each category and calculates the cosine similarity between the current article vector and the candidate article vectors. Standard cosine similarity is used here to measure the degree of proximity in the semantic space. Two key filtering mechanisms are introduced during the calculation: the first is "self-similarity filtering," used to remove entries that are almost identical to the current article; the second is "same series filtering," used to prevent the same content series from appearing repeatedly in the recommendation list.

After initial screening, the system ranks candidate articles based on similarity and further incorporates a diversity control mechanism to prevent recommendations from being dominated by a single topic or structure. Notably, the system directly removes all content belonging to the same series as the current article, ensuring a "cross-series distribution" in the content structure of the recommendations. Based on this, a maximum of four representative articles are retained for each category, and the average similarity of the candidate articles is used as the overall weight for that category in subsequent ranking.

After all categories have completed weight calculation and sorting, the system ultimately selects the Top 3 categories as the first layer of content displayed in the right-hand menu to construct the user's semantic entry space.

During the WordPress rendering phase, these results are generated throughout the post's lifecycle and output to the front end along with the page: on one hand, PHP integrates pre-computed semantic data (embedding similarity results, category-space classification space, and series rule information) into the final recommendation structure; on the other hand, it injects the HTML container required for the right-hand menu into the page, and through... window.SIDEBAR_DATA Exposed to the front-end JavaScript.

The front-end JavaScript only needs to read this structured data to complete the interactive rendering of the category display and article list, without participating in any semantic calculation or data generation process.

From an implementation perspective, this PHP layer is not an independent recommendation calculation engine, but a "semantic result combination layer": it is responsible for lightweightly integrating the offline-built embedding index with the classification semantic space, and combining it with the current article context to complete the final candidate selection and structure organization, thereby achieving stable semantic recommendation capabilities without introducing independent backend services.

The specific PHP code is as follows:

// ======================================================
// ① URL normalize
// ======================================================
function sidebar_normalize_url(url) {path = parse_url(url, PHP_URL_PATH);
    return rtrim(path ?? '', '/');
}

// ======================================================
// ② cosine similarity
// ======================================================
function sidebar_cosine(a,b) {
    dot = 0;na = 0;
    nb = 0;n = min(count(a), count(b));

    for (i = 0;i < n;i++) {
        dot +=a[i] *b[i];na  += a[i] * a[i];
        nb  +=b[i] *b[i];
    }

    returndot / (sqrt(na) * sqrt(nb) + 1e-10);
}

// ======================================================
// ③ series extractor(完全复用成功逻辑)
// ======================================================
function sidebar_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 =map[index_cn] ?? 0;

        if (index > 0) {
            return [
                'series_key' => series_key,
                'series_index' =>index
            ];
        }
    }

    // Part N 版本
    if (preg_match('/^(.*?)(?:Part\s*|part\s*|\()(\d+)\)?/u', title,matches)) {

        return [
            'series_key' => trim(matches[1]),
            'series_index' => intval(matches[2])
        ];
    }

    return null;
}

// ======================================================
// ④ 从 article-index.json 构建 series map(关键)
// ======================================================
function sidebar_build_series_map_from_articles() {

    file = get_template_directory() . '/cache/article-index.json';

    if (!file_exists(file)) return [];

    articles = json_decode(file_get_contents(file), true);
    if (!articles) return [];map = [];

    foreach (articles asid => article) {

        if (empty(article['url']) || empty(article['title'])) continue;url = sidebar_normalize_url(article['url']);info = sidebar_extract_series_info(article['title']);

        if (info) {
            map[url] = info['series_key'];
        }
    }

    returnmap;
}

// ======================================================
// ⑤ 数据层(最终稳定版)
// ======================================================
function sidebar_build_data() {

    embedding_file = get_template_directory() . '/cache/embedding-index.json';category_file  = get_template_directory() . '/cache/category-space.json';

    embeddings = json_decode(file_get_contents(embedding_file), true);
    categories = json_decode(file_get_contents(category_file), true);

    if (!embeddings || !categories || !isset(categories['categories'])) {
        return null;
    }

    // embedding mapemb_map = [];

    foreach (embeddings asitem) {
        if (!isset(item['url'],item['embedding'])) continue;
        emb_map[sidebar_normalize_url(item['url'])] = item['embedding'];
    }

    // series map(来自 article-index.json ——关键修复点)series_map = sidebar_build_series_map_from_articles();

    current_url = sidebar_normalize_url(_SERVER['REQUEST_URI'] ?? '');
    current_vec =emb_map[current_url] ?? null;

    if (!current_vec) {

        all = array_values(emb_map);
        if (!all) return null;dim = count(all[0]);avg = array_fill(0, dim, 0);

        foreach (all as vec) {
            for (i = 0; i<dim; i++) {avg[i] +=vec[i];
            }
        }

        for (i = 0; i<dim; i++) {avg[i] /= count(all);
        }

        current_vec =avg;
    }

    current_series =series_map[current_url] ?? null;SELF_SIM_THRESHOLD = 0.985;

    categories_out = [];

    foreach (categories['categories'] as cat) {candidates = [];
        sum = 0;cnt = 0;

        foreach (cat['articles'] asa) {

            url = sidebar_normalize_url(a['url']);
            if (!isset(emb_map[url])) continue;

            vec =emb_map[url];score = sidebar_cosine(current_vec,vec);

            // ① 去自相似
            if (score>SELF_SIM_THRESHOLD) {
                continue;
            }

            // ② 🚨 彻底去同系列(基于 article-index.json)
            series =series_map[url] ?? null;

            if (current_series && series &&series === current_series) {
                continue;
            }candidates[] = [
                'title' => a['title'],
                'url'   =>a['url'],
                'score' => score,
                'series'=>series
            ];

            sum +=score;
            cnt++;
        }

        if (cnt === 0) continue;

        usort(candidates, fn(a,b) =>b['score'] <=> a['score']);

        // diversity(每个 series 只保留一个)final = [];
        used = [];

        foreach (candidates as item) {s = item['series'];

            if (s && isset(used[s])) continue;

            final[] = [
                'title' =>item['title'],
                'url'   => item['url'],
                'score' =>item['score']
            ];

            if (s)used[s] = true;

            if (count(final) >= 4) break;
        }

        if (!final) {final = array_slice(candidates, 0, 4);
        }categories_out[] = [
            'id' => cat['id'],
            'name' =>cat['name'],
            'score' => sum / max(cnt, 1),
            'articles' => final
        ];
    }

    usort(categories_out, fn(a,b) => b['score'] <=>a['score']);

    return array_slice(categories_out, 0, 3);
}

// ======================================================
// ⑥ UI
// ======================================================
function render_sidebar_html() {
?>
<div id="semantic-sidebar">
    <div class="ssb-panel">
        <div class="ssb-trigger">查看相关分类·3个匹配</div>
        <div class="ssb-level ssb-level-1"></div>
        <div class="ssb-level ssb-level-2" id="ssb-articles"></div>
    </div>
</div>
<?php
}

// ======================================================
// ⑦ WP Hook(必须保留)
// ======================================================
add_filter('the_content', function (content) {
    if (!is_single()) return content;data = sidebar_build_data();

    ob_start();

    if (data) {
        echo '<script>';
        echo 'window.SIDEBAR_DATA=' . json_encode(
            ['categories' =>data],
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
        );
        echo '</script>';
    }

    render_sidebar_html();
    sidebar = ob_get_clean();
    returncontent . $sidebar;
});

3.4.3 Implementation of the right-side menu interaction logic (sidebar.js)

This section is the "interaction control layer" for the right-side floating menu when it runs on the front end; the corresponding file is... sidebar.jsIts function is not to generate data, nor to participate in any semantic calculations, but rather to inject structured data into the page using PHP.window.SIDEBAR_DATAThis is transformed into interactive UI behaviors, thus completing the final step of the "data → visual → behavioral feedback" process.

From an overall implementation perspective, this JS logic can be broken down into three core steps: initial rendering, category switching control, and floating state management.

First, during the initialization phase, the script will retrieve global variables. window.SIDEBAR_DATA The system reads the data structure generated and injected by PHP during the article's lifecycle and locates two key DOM containers in the right-hand menu: the primary category container (.ssb-level-1) and article display panel (#ssb-articlesSubsequently, the script dynamically generates HTML based on the category data, rendering all categories as hoverable entry nodes. This step is essentially a typical data-driven DOM construction process, and it is executed only once during the initialization phase, thus avoiding state pollution caused by repeated rendering.

After completing the basic rendering, the system enters its core interactive logic: category switching and article display. When the user hovers the mouse over a category node, JavaScript captures the target node through an event delegation mechanism and displays the article based on its context. data-id Locate the corresponding category in the data structure, and then call... showCategory() The article list under this category is dynamically rendered to the floating panel on the right. This process is accomplished by directly rewriting the DOM, thus enabling instant switching between categories.

At the same time, the system also maintains a lightweight runtime state, including the currently active category ID (activeCatId) and timers for delayed shutdown (hideTimerThese states exist not to implement complex logic, but to ensure the stability of hover interactions and avoid flickering or accidental closing when the mouse moves quickly.

At the interaction control level, this version adopts a hybrid model of "hover trigger + state-driven". When the mouse enters the sidebar area, it will trigger... openSidebar()This activates the primary category container.active The mouse can be moved to display a list of categories. When the mouse leaves the sidebar area, a short delay timer is started to close the window, reducing accidental clicks and flickering when moving the mouse over the edge.

In the category interaction section, when a user hovers the mouse over a category node, the system identifies the target node based on an event delegation mechanism and then... data-id Match the corresponding category data, then call... showCategory() Update the content of the article panel on the right. Because the article panel uses a dynamic DOM rewriting strategy, the currently displayed content will be directly replaced every time the category changes.

Meanwhile, to ensure the continuity of interaction, the system has added a "closing protection mechanism" to the article panel area: when the mouse enters the article panel, the delayed closing timer is cleared to prevent the panel from being accidentally closed during reading; when the mouse leaves the article area, the closing logic is restored and the internal state is reset.

It is important to note that this version's initialization mechanism has been expanded from the traditional DOMContentLoaded mode to "Argon PJAX lifecycle driven". Through window.pjaxLoaded The hook allows the system to re-execute the initialization logic after each PJAX page switch, thereby ensuring... SIDEBAR_DATA It stays in sync with the current article context and avoids state residue issues caused by partial page replacement.

It's important to emphasize that this layer still doesn't involve any recommendation calculation logic, nor does it participate in classification ranking or similarity calculation. All semantic calculations and recommendation result generation are completed on the PHP side; JavaScript's role is limited to display control and interactive responses.

Overall,sidebar.js It's more like a lightweight UI state controller based on the PJAX lifecycle. It implements a stable floating menu interaction model through a very small number of state variables and event listening mechanisms, so that the right-side menu still has clear and predictable behavioral semantics even without front-end framework dependencies.

The code for sidebar.js is as follows:

(function () {

    let initialized = false;

    function init() {

        const root = document.getElementById("semantic-sidebar");
        if (!root) return;

        const data = window.SIDEBAR_DATA;
        if (!data || !data.categories) return;

        const level1 = root.querySelector(".ssb-level-1");
        const panel  = root.querySelector("#ssb-articles");

        let activeCatId = null;
        let sidebarOpen = false;
        let hideTimer = null;

        // =========================
        // 防止重复初始化(同一 PJAX 生命周期内)
        // =========================
        if (initialized) return;
        initialized = true;

        // =========================
        // render categories
        // =========================
        level1.innerHTML = data.categories.map(cat =>
            `<div class="ssb-cat" data-id="{cat.id}">{cat.name}
            </div>`
        ).join("");

        // =========================
        // open sidebar
        // =========================
        function openSidebar() {
            sidebarOpen = true;
            level1.classList.add("active");
        }

        // =========================
        // close sidebar
        // =========================
        function closeSidebar() {
            sidebarOpen = false;
            level1.classList.remove("active");
            hidePanel();
        }

        // =========================
        // show articles
        // =========================
        function showCategory(cat) {

            activeCatId = cat.id;

            panel.innerHTML = cat.articles.map(a =>
                `<a href="{a.url}">{a.title}</a>`
            ).join("");

            panel.classList.add("active");
        }

        // =========================
        // hide articles
        // =========================
        function hidePanel() {
            panel.classList.remove("active");
            panel.innerHTML = "";
            activeCatId = null;
        }

        // =========================
        // hover entry
        // =========================
        root.addEventListener("mouseenter", () => {
            openSidebar();
        });

        // =========================
        // hover leave
        // =========================
        root.addEventListener("mouseleave", () => {

            hideTimer = setTimeout(() => {
                closeSidebar();
            }, 120);
        });

        // =========================
        // category hover
        // =========================
        level1.addEventListener("mouseenter", (e) => {

            const el = e.target.closest(".ssb-cat");
            if (!el) return;

            const catId = el.dataset.id;
            const cat = data.categories.find(c => c.id === catId);

            if (!cat) return;

            showCategory(cat);
        }, true);

        // =========================
        // panel protection
        // =========================
        panel.addEventListener("mouseenter", () => {
            clearTimeout(hideTimer);
        });

        panel.addEventListener("mouseleave", () => {
            hidePanel();
        });

    }

    // =========================
    // ⭐ 核心修复:Argon PJAX 生命周期入口
    // =========================
    window.pjaxLoaded = function () {

        // 每次 PJAX 页面切换都会执行这里
        initialized = false;
        init();
    };

    // =========================
    // 首次加载(非 PJAX)
    // =========================
    if (document.readyState === "loading") {
        document.addEventListener("DOMContentLoaded", init);
    } else {
        init();
    }

})();

Additionally, a separate piece of PHP code needs to be added to implement the call to sidebar.js:

add_action('wp_enqueue_scripts', function () {

    wp_enqueue_script(
        'semantic-sidebar',
        get_template_directory_uri() . '/cache/sidebar.js',
        array(),
        null,
        true
    );

});

Note: I've placed sidebar.js and other JSON files that need to be directly called by WordPress all in WordPress.”/wp-content/themes/argon-theme-master/cache/“"Under the path".

3.4.4 Right-side menu style and visual presentation (CSS)

This part is the visual representation layer of the right-side floating menu in the WordPress page, and the corresponding implementation file is a CSS stylesheet. Its role is not to participate in any interactive logic, nor is it involved in data processing. Instead, it is responsible for transforming the structured DOM generated by JS into a floating UI component with a sense of hierarchy and space, thereby completing the final "visual presentation" of the entire system.

From an overall structural perspective, this part of the CSS is mainly organized around three levels: the overall floating container (Sidebar Root), the Level 1 Categories area, and the Level 2 Article Panel. Each level undertakes a different visual responsibility, thus forming a clear spatial hierarchy.

First is the outermost container. #semantic-sidebarIt uses position: fixed Fixed to the center right side of the page, and through transform: translateY(-52%) Achieve vertical center offset to maintain a stable visual position across different screen heights. The entire component visually belongs to a "floating UI," hence the relatively high offset. z-indexThis ensures that it will not be covered by the main text content.

Inside the container,.ssb-panel It serves as the overall background and visual anchor. It achieves this through a semi-transparent background color and... backdrop-filter Achieving a frosted glass effect, combined with shadows and borders, creates a lightweight yet layered card-like container. The design goal for this layer is "to exist without interfering," meaning to minimize visual intrusion on the main text while ensuring visibility.

Interactive entry point .ssb-trigger As the initial tooltip area for the component, it provides only a low-obtrusive visual entry point. It does not participate in the expansion logic itself, but only provides lightweight visual guidance, and adds a status indicator point through pseudo-elements, allowing users to quickly recognize the existence of the component's function.

In terms of structural control, primary classification containers .ssb-level The display logic is entirely controlled by JavaScript, so CSS is only responsible for basic state definitions: default hidden.display: none), and add in JS .active The display then switches. This design avoids conflicts between CSS hover and JS states, making the expansion behavior completely predictable.

Category Items .ssb-cat This layer is primarily responsible for providing interactive visual feedback, including hover highlighting, text truncation, and lightweight transition effects. The design goal of this layer is to maintain clear readability even with a large number of categories, while avoiding visual clutter.

Second-level article panel #ssb-articles This is the most visually complex part of the entire component. It is designed as a floating panel relatively positioned to the left of the category list, through... position: absolute Spatial anchoring is implemented to ensure it maintains a spatial relationship with the current category when expanded. Its display and hiding also rely entirely on JavaScript for addition or removal. .active This avoids the jittering problem caused by hover triggering.

In the article entry .ssb-articles a In its implementation, a combination of "double-line truncation + fade-out mask" was adopted, through... -webkit-line-clamp By controlling the text length and using mask gradients to avoid abrupt text breaks, this approach maintains both information density and overall visual neatness.

Overall, the core design principle of this CSS version is "weak interactivity, strong structure". All interactive behaviors are controlled by JS, and CSS is only responsible for expressing the visual results after state changes. This avoids the coupling problem between hover and script logic, making the entire right-side menu system more stable in behavior and more visually consistent.

The CSS code is as follows (the "Extra CSS" section for the current WordPress theme):

/* ======================== Root (remains unchanged) ========================= */ #semantic-sidebar { position: fixed; right: 16px; top: 50%; transform: translateY(-52%); z-index: 99999; width: 200px; font-size: 13px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; } /* ========================= Panel container (stabilizes the visual layer) ========================= */ .ssb-panel { background: rgba(28, 30, 34, 0.72); color: #e8e8e8; border-radius: 12px; padding: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.35), 0 0 0 1px rgba(255,255,255,0.04); backdrop-filter: blur(12px) saturate(140%); -webkit-backdrop-filter: blur(12px) saturate(140%); border: 1px solid rgba(255,255,255,0.06); position: relative; } /* ========================= Trigger (entry button) ========================= */ .ssb-trigger { padding: 10px 8px; background: rgba(255,255,255,0.06); border-radius: 10px; cursor: default; user-select: none; font-weight: 500; color: #ddd; display: flex; align-items: center; justify-content: center; position: relative; } .ssb-trigger::after { content: ""; position: absolute; right: 10px; top: 50%; width: 6px; height: 6px; border-radius: 50%; background: rgba(120, 180, 255, 0.6); transform: translateY(-50%); opacity: 0.6; } .ssb-trigger:hover { background: rgba(255,255,255,0.1); color: #fff; } /* ========================= Level container (critical fix) ✔ Completely controlled by JS ========================= */ .ssb-level { display: none; /* Hide by default */ margin-top: 10px; } /* Expanded by JS */ .ssb-level-1.active { display: block; } /* ========================= Category item ========================= */ .ssb-cat { padding: 7px 8px; border-radius: 8px; cursor: pointer; transition: all 0.15s ease; color: #cfcfcf; display: block; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .ssb-cat:hover { background: rgba(255,255,255,0.08); color: #fff; } /* ========================= Level 2 panel (fully stable) ========================= */ #ssb-articles { position: absolute; right: 100%; top: 0; width: 220px; margin-right: 10px; background: rgba(28, 30, 34, 0.75); border-radius: 12px; padding: 10px; box-shadow: 0 10px 28px rgba(0,0,0,0.45); backdrop-filter: blur(14px) saturate(150%); -webkit-backdrop-filter: blur(14px) saturate(150%); border: 1px solid rgba(255,255,255,0.06); display: none; pointer-events: auto; } /* JS control display*/ #ssb-articles.active { display: block; } /* =========================== Article items (2 lines limit) ========================= */ #ssb-articles a { position: relative; display: block; padding: 6px 8px; border-radius: 8px; color: #bdbdbd; text-decoration: none; line-height: 1.35; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; -webkit-mask-image: linear-gradient( to bottom, black 85%, transparent 100% ); mask-image: linear-gradient( to bottom, black 85%, transparent 100% ); } #ssb-articles a:hover { background: rgba(255,255,255,0.08); color: #fff; transform: none; } /* ========================= cleanup ========================= */ .ssb-level-1 { padding-bottom: 0; } /* ========================== Disable right-side menu on mobile devices ========================= */ @media (max-width: 768px) { #semantic-sidebar { display: none !important; } }

Initialization issues in a PJAX environment

It should be noted that the above implementation is based on the traditional page loading model by default, which means that the JavaScript initialization logic will be re-executed when switching pages.

However, if the website uses refresh-free loading technologies such as PJAX (PushState + AJAX), then during page transitions, typically only a portion of the DOM content is replaced, without reloading the entire page. Therefore, dependency DOMContentLoaded The initialization logic of the script is usually only executed once when the page is visited for the first time, and will not be triggered again when the page is switched later.

Taking the Argon theme as an example, its official documentation clearly states:

window.pjaxLoaded = function () {
    // 页面每次切换完成后执行
};

Therefore, when using PJAX themes such as Argon, it is necessary to attach the component initialization logic to the corresponding PJAX lifecycle hooks so that it is re-executed after each page switch. For example, the final version of this article sidebar.js This method was adopted:

window.pjaxLoaded = function () {
    initialized = false;
    init();
};

This ensures that whether the page is loaded for the first time or an article is switched via PJAX, the right-hand menu can correctly read the content corresponding to the current page. SIDEBAR_DATA And complete the reinitialization.

From an engineering perspective, this isn't actually a problem with the right-side menu itself, but rather a problem with front-end component lifecycle management in a PJAX environment. Any custom module that depends on page initialization needs to be adapted according to the lifecycle hooks provided by the theme or framework.


3.4.5 Right-side menu operation and interaction

Once the entire system has loaded, only a lightweight floating entry component will appear on the right side of the page. This component initially does not expand the category list or display any article content; its visual presence is deliberately minimized, retaining only a stable entry area to avoid interfering with reading the main text.

image.png

When the user moves the mouse over the floating area on the right, the system enters an interactive activation state. At this time, the primary category list is dynamically expanded and presented as a vertical list within the floating window. The entire expansion process does not rely on complex animations but is presented through stable state transitions, ensuring predictable interface behavior.

image.png

In this state, users can directly switch content by hovering the mouse over different categories. Each category corresponds to a semantic clustering result, and below it is a list of the top 4 recommended articles within that category:

image.png

As users continue to move between categories, the article panel on the right updates its content in real time, without any overall layout jitter or page shifting. The article list maintains a fixed width and position, with only the content changing, thus ensuring visual stability. This design makes the interaction more like an "information preview switch" rather than a traditional menu expansion.

image.png

Additionally, when the mouse moves away from the sidebar area, the system triggers a delayed shutdown mechanism to prevent the panel from flickering frequently or disappearing suddenly due to slight accidental touches. After the user completely leaves the interactive area, the category list and article panel will return to their initial collapsed state.


4 Series Retrospective and Development Outlook

If we look back at the series on "building lightweight knowledge indexes for blogs" over a longer timescale, it is not actually a collection of implementations centered around a single function, but rather a gradual answer to a more fundamental question: how to transform blog content from a "collection of independent articles" into an "information structure that can be continuously reorganized by machines".

In this process, each article seems to be solving a relatively independent problem—related article recommendations, series cards, guess what you want, AI summaries, and semantic menus on the right—but if we abstract them to the same level, we can see that they share the same evolutionary path: from explicit structure to implicit semantics, and then to computable content organization.

From an overall structural perspective, this series can be summarized into three progressively stronger levels:

Content Layer: WordPress Original Post Structure Layer (Information Organization System): Related Posts / Series Cards / Right-hand Menu / Category System / Series Relational Semantic Enhancement Layer: Embedding / Similarity Calculation / AI Summary / "You May Also Like" (Deleted)

The core of the change lies not in the increase in the number of functions, but in the gradual clarification of the boundary between the structural layer and the semantic layer. Early systems relied more on explicit rules (tags, categories, manual associations), but after the introduction of embedding, the relationships between content began to gradually shift from "human-defined" to "semantic computation-driven".

In this system, the semantic menu on the right occupies a relatively special position: it does not introduce new semantic capabilities, but rather reorganizes the existing embedding similarity results and classification space into an interactive entry point that closely aligns with the user's browsing path. From a system perspective, it is more like a "mapping of semantic structure to the user entry layer" rather than an independent computation module.

If we break it down from a functional perspective:

  • “"Related Articles" solves the problem of local semantic neighborhood (embedding similarity).
  • “"Series cards" solve the problem of strongly structured sequences (explicit order).
  • “The "Guess What You Think" feature offered weak semantic inference capabilities (fuzzy matching), but since its functionality was already included in the right-hand menu, it has been removed.
  • “"AI Summary" provides content compression and readable expression.
  • “The "right-side menu" is a semantic entry point and category navigation built on top of the existing structure, providing continuous accessibility.

From this perspective, this series is not a functional overlap, but a structural evolution that gradually unfolds around different organization and access methods of the same content.

Meanwhile, the system maintains a clear engineering constraint: semantic computation is split into offline building and runtime lightweight processing, with the WordPress front-end only handling data loading and display logic. This model allows the system to maintain scalability while still offering low operating costs and stability.

But it is precisely after this stage that a boundary begins to become clear: the current system has completed the basic construction of "semantic expression and structural organization", but has not yet entered the stage of "semantic-driven content flow".

In other words, existing modules are already able to express the relationships between content quite well, but most of these relationships still come from pre-computation and rule combination, and have not yet formed a continuously evolving semantic generation mechanism.

If we place this series within a longer evolutionary path, its current work can be understood as the construction of a foundational framework: transforming blog content from isolated text into computable objects with stable semantic structures.

Upon this foundation, there are still some natural directions of evolution, such as:

  • From article-level semantics to more granular content unit organization
  • From static embedding similarity to dynamic ranking based on behavioral feedback
  • From pre-computed indexes to partial online update mechanisms
  • From display-based recommendations to path-based content discovery

However, these goals are beyond the scope of the current stage. Therefore, from the perspective of the series as a whole, what these seven articles truly accomplish is not a "feature set," but a relatively complete intermediate layer design: introducing a stable, scalable, and low-cost semantic structure layer between content and presentation. The significance of this layer lies not only in functional enhancement, but also in enabling content to "be reorganized" for the first time.

At this point, the "Building a Lightweight Knowledge Index for Blogs" series has completed a phase of structural closure.

📚 系列文章:为博客构建“轻量级知识索引”(7 / 7)

📌 Content Structure Hints:
This content belongs to "AI Learning MapThis is part of the document; you can view the full content path here: AI Learning Map .
View related categories · 3 matches
📎 Related Articles
Share this article
All blog content is original; please indicate the source when reprinting! The blog's RSS address is:https://blog.tangwudi.com/feed, welcome to subscribe; if necessary, you can joinTelegram GroupDiscuss the problem together.
No Comments

Send Comment Edit Comment


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠(ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ°Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
Emoticons
Emoji
Little Dinosaur
flower!
Previous
       

👋 Welcome to "Invincible Personal Blog"“

This section will focus on long-term exploration in the following areas:

🧱 Building Personal Digital Infrastructure and Blog Systems
☁️ Cloudflare and Network Architecture Practices
🧠 Exploring AI and Knowledge Systems
🛡️ Network security and access optimization
🎵 Music and Sound Cognition
👁️ Cognitive Perspective and Worldview