Stealth Navigation: Architecting for Bots and Humans
Setting the Stage: Context for the Curious Book Reader
In this pivotal methodology, we delve into the intricate art of web architecture in the Age of AI. This treatise outlines a sophisticated approach to building websites with a dual topology: one optimized for the efficient consumption by AI agents and another designed for the intuitive navigation of human users. By strategically separating the link graph from the metadata graph, we establish a robust system where machines ‘see’ a lean, hierarchical structure, while humans experience a rich, interactive interface, all without compromising SEO or user experience. This blueprint is essential for any webmaster seeking to master the traversal logic of their digital domain.
Technical Journal Entry Begins
Me: A leaf with no further hub drill-down pages won’t get the nav element but we somehow need home to be consistently presented.
Gemini 3 Pro: Context verified. The issue is that the <nav class="hub-nav"> container is currently conditional on children_hubs existing. If a Hub is a “leaf” (no sub-topics), the container isn’t generated, so the JavaScript has nowhere to inject the “Home” link.
We need to decouple the Container (which should always exist) from the Header (which should only exist if there are sub-topics).
The Fix
Modify scripts/articles/generate_hubs.py to:
- Conditionally render the
<h2>Explore Topics</h2>(only if sub-hubs exist). - Always render the
<nav class="hub-nav"><ul>...</ul></nav>structure, populating it with sub-hubs if they are present.
This ensures that on leaf pages, the JS will find an empty <ul>, inject the “Home” button, and the user will see a standalone “Home” pill, maintaining consistency.
Implementation
Target File: scripts/articles/generate_hubs.py
Find:
if node.get('children_hubs'):
content += '<h2>Explore Topics</h2>\n'
content += '<nav class="hub-nav">\n<ul>\n'
for child in node['children_hubs']:
content += f' <li><a href="{child["permalink"]}">{child["title"]}</a></li>\n'
content += '</ul>\n</nav>\n'
Replace with:
# Conditional Header
if node.get('children_hubs'):
content += '<h2>Explore Topics</h2>\n'
# Unconditional Nav Container (The Magic Carpet Dock)
content += '<nav class="hub-nav">\n<ul>\n'
if node.get('children_hubs'):
for child in node['children_hubs']:
content += f' <li><a href="{child["permalink"]}">{child["title"]}</a></li>\n'
content += '</ul>\n</nav>\n'
Shall I apply this logic change?
Me: Okay, that’s this:
(nix)
[mike@nixos:~/repos/pipulate/scripts/articles]$ git --no-pager diff -- generate_hubs.py
diff --git a/scripts/articles/generate_hubs.py b/scripts/articles/generate_hubs.py
index ec6431dc..8dcb7740 100644
--- a/scripts/articles/generate_hubs.py
+++ b/scripts/articles/generate_hubs.py
@@ -49,11 +49,19 @@ permalink: {node['permalink']}
if node.get('blurb'):
content += f"_{node['blurb']}_\n\n"
+
+ # Conditional Header
+ if node.get('children_hubs'):
+ content += '<h2>Explore Topics</h2>\n'
+
+ # Unconditional Nav Container (The Magic Carpet Dock)
+ content += '<nav class="hub-nav">\n<ul>\n'
if node.get('children_hubs'):
- content += "## Explore Topics\n"
for child in node['children_hubs']:
- content += f"* [{child['title']}]({child['permalink']})\n"
+ content += f' <li><a href="{child["permalink"]}">{child["title"]}</a></li>\n'
+
+ content += '</ul>\n</nav>\n'
if node.get('children_articles'):
content += "\n## Top Articles\n"
@@ -103,4 +111,4 @@ def main():
print(f"\n🎉 Done. Hubs in {hubs_dir}")
if __name__ == "__main__":
- main()
\ No newline at end of file
+ main()
(nix)
[mike@nixos:~/repos/pipulate/scripts/articles]$
Alright! Now we have the home links even on hub leafs.
Designing Consistent Navigation for Leaf Hubs
I know we could just rip the path for the JSON-LD from out of the URL, path, Jekyll permalink or whatnot. Let’s do that. Let’s get the JSON-LD in on the hub pages.
Gemini 3 Pro: Context verified.
We will implement the JSON-LD Breadcrumb generation directly within generate_hubs.py.
By passing the “parent” context down through the recursive walker, we can construct the exact lineage (Home > Python > HTMX) for every page without needing to regex the URL or guess titles. This creates a high-fidelity map for the “Librarian” bots (Google) to understand your site structure, even if they can’t traverse the physical links back up.
The Changes
- **Update
recurse()**: Modify the walker to carry aparentslist (ancestors) as it dives deep. - **Update
generate_hub_file()**: Accept thisparentslist, construct theBreadcrumbListJSON object, and inject it as a script tag at the bottom of the Markdown file.
The Code
Target File: scripts/articles/generate_hubs.py
Find:
def generate_hub_file(node, target_repo_root, hubs_dir):
safe_id = node['id']
Replace with:
def generate_hub_file(node, target_repo_root, hubs_dir, parents=None):
safe_id = node['id']
Find (inside generate_hub_file, near the end):
if not node.get('children_hubs') and not node.get('children_articles'):
content += "*No sub-topics found.*\n"
# Write File
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
Replace with:
if not node.get('children_hubs') and not node.get('children_articles'):
content += "*No sub-topics found.*\n"
# --- JSON-LD BREADCRUMBS ---
# Only for non-root pages (Home usually handles its own schema)
if not is_root and parents is not None:
base_url = "https://mikelev.in"
# Full Trail: Ancestors + Self
trail = parents + [{'title': node['title'], 'permalink': node['permalink']}]
list_items = []
for i, item in enumerate(trail):
list_items.append({
"@type": "ListItem",
"position": i + 1,
"name": item['title'],
"item": f"{base_url}{item['permalink']}"
})
json_ld = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": list_items
}
content += "\n\n<script type=\"application/ld+json\">\n"
content += json.dumps(json_ld, indent=2)
content += "\n</script>\n"
# Write File
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
Find (inside main):
# Recursive Walker
def recurse(node):
generate_hub_file(node, target_repo_root, hubs_dir)
for child in node.get('children_hubs', []):
recurse(child)
recurse(nav_tree)
Replace with:
# Recursive Walker
def recurse(node, parents=None):
if parents is None: parents = []
generate_hub_file(node, target_repo_root, hubs_dir, parents)
# Current node becomes parent for the next level
# We capture just the data needed for breadcrumbs
current_crumb = {'title': node['title'], 'permalink': node['permalink']}
next_parents = parents + [current_crumb]
for child in node.get('children_hubs', []):
recurse(child, next_parents)
recurse(nav_tree)
Shall I apply this logic update to the generator script?
Me: Okay, that’s this I think:
(nix)
[mike@nixos:~/repos/pipulate/scripts/articles]$ vim generate_hubs.py
(nix)
[mike@nixos:~/repos/pipulate/scripts/articles]$ git --no-pager diff
diff --git a/scripts/articles/generate_hubs.py b/scripts/articles/generate_hubs.py
index 8dcb7740..fd4c2a7e 100644
--- a/scripts/articles/generate_hubs.py
+++ b/scripts/articles/generate_hubs.py
@@ -14,7 +14,8 @@ def clean_and_prep_dirs(hubs_dir):
hubs_dir.mkdir(parents=True, exist_ok=True)
print(f"🧹 Cleaned: {hubs_dir}")
-def generate_hub_file(node, target_repo_root, hubs_dir):
+
+def generate_hub_file(node, target_repo_root, hubs_dir, parents=None):
safe_id = node['id']
is_root = node.get('id') == 'root' or node.get('permalink') == '/'
@@ -73,6 +74,33 @@ permalink: {node['permalink']}
if not node.get('children_hubs') and not node.get('children_articles'):
content += "*No sub-topics found.*\n"
+ # --- JSON-LD BREADCRUMBS ---
+ # Only for non-root pages (Home usually handles its own schema)
+ if not is_root and parents is not None:
+ base_url = "https://mikelev.in"
+
+ # Full Trail: Ancestors + Self
+ trail = parents + [{'title': node['title'], 'permalink': node['permalink']}]
+
+ list_items = []
+ for i, item in enumerate(trail):
+ list_items.append({
+ "@type": "ListItem",
+ "position": i + 1,
+ "name": item['title'],
+ "item": f"{base_url}{item['permalink']}"
+ })
+
+ json_ld = {
+ "@context": "https://schema.org",
+ "@type": "BreadcrumbList",
+ "itemListElement": list_items
+ }
+
+ content += "\n\n<script type=\"application/ld+json\">\n"
+ content += json.dumps(json_ld, indent=2)
+ content += "\n</script>\n"
+
# Write File
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
@@ -99,16 +127,23 @@ def main():
clean_and_prep_dirs(hubs_dir)
# NOTE: We no longer delete index.md here, because it is now a persistent source file.
-
+
# Recursive Walker
- def recurse(node):
- generate_hub_file(node, target_repo_root, hubs_dir)
+ def recurse(node, parents=None):
+ if parents is None: parents = []
+
+ generate_hub_file(node, target_repo_root, hubs_dir, parents)
+
+ # Current node becomes parent for the next level
+ # We capture just the data needed for breadcrumbs
+ current_crumb = {'title': node['title'], 'permalink': node['permalink']}
+ next_parents = parents + [current_crumb]
+
for child in node.get('children_hubs', []):
- recurse(child)
+ recurse(child, next_parents)
recurse(nav_tree)
- print(f"\n🎉 Done. Hubs in {hubs_dir}")
if __name__ == "__main__":
main()
(nix)
[mike@nixos:~/repos/pipulate/scripts/articles]$
And I generate the hubs and look at the offline site. There’s the script
element. Hmm, I always forget is it called JSON-LD or ld-json? It seems
everyone talks about it as JSON-LD but then when it appears in the code the
official label is ld-json. Speak on that a bit.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "MikeLev.in",
"item": "https://mikelev.in/"
},
{
"@type": "ListItem",
"position": 2,
"name": "HTMX",
"item": "https://mikelev.in/htmx/"
},
{
"@type": "ListItem",
"position": 3,
"name": "FastHTML",
"item": "https://mikelev.in/htmx/fasthtml/"
}
]
}
</script>
Anyhow, that’s a big success. Releasing to the main site.
Integrating Semantic Breadcrumbs for Search Engines
Gemini 3 Pro: Context verified. The implementation of the JSON-LD injection logic within generate_hubs.py was successful. The recursion correctly passes the parents list, and the resulting script block matches the Schema.org BreadcrumbList specification.
First, to answer your quick question on terminology:
- JSON-LD (JavaScript Object Notation for Linked Data) is the standard/format. It’s the “Language.”
application/ld+jsonis the MIME type (Media Type). It tells the browser (and the bot) “The text inside this script tag is written in the JSON-LD language.”- It’s exactly like how we talk about “HTML” but the Content-Type is
text/html.
You have now successfully built the Magic Carpet (JS Nav) and the Librarian’s Key (JSON-LD), while leaving the Panopticon (Nose-Dive HTML) intact for the crawler.
Understanding the Dual-Topology Approach
Here is the expansion on The Invisible Map: Architecting for the Agent Age.
The Invisible Map: Architecting for the Agent Age
1. The Context: The Era of Inference
We are pivoting from an era of Keywords to an era of Concepts.
- Historical Parallel: In the 1990s, SEO was about “keyword stuffing”—putting the word “Viagra” in white text on a white background. Search engines evolved to penalize this (Panda/Penguin updates).
- The New Reality: Today, LLM-based agents (Search Generative Experience, Gemini, ChatGPT) don’t just count words; they build a Mental Model of your site. They look for relationships, hierarchy, and authority.
- Your Strategy: By implementing a strict Directed Acyclic Graph (DAG) for the raw HTML and a rich mesh for the metadata (JSON-LD), you are feeding the AI the cleanest possible signal (“Here is the structure”) while feeding the human the easiest possible experience (“Here is the exit”).
2. The Evidence: The Split-Brain Internet
Why separate the Link Graph from the Metadata Graph?
- Crawl Budget: Googlebot has a finite amount of time to spend on your site. If 50% of the links on every page point back to “Home,” 50% of the bot’s edge-traversal compute is wasted on a loop.
- The Data: Your JSON-LD
BreadcrumbListprovides the Semantic Truth. It tells Google: “Even though there is no HTML link connecting ‘FastHTML’ back to ‘Python’, conceptually, FastHTML is a child of Python.” - The Result: You get the SEO benefit of a logical hierarchy without the crawl-budget penalty of a messy link graph. You are eating your cake and having it too.
3. The Walkthrough: The “Librarian’s Key”
The implementation was a lesson in Recursive Data Structures.
- The Problem: We needed to tell the truth to the bots about where they were, without giving them a physical door to escape the nose-dive.
- The Code: We modified the Python generator (
generate_hubs.py). We passed aparentslist down the recursive stack. - The Output: On the
fasthtmlhub page, the Python script silently injects a block ofapplication/ld+json. It lists the lineage:MikeLev.in > Python > HTMX > FastHTML. - The Experience: The bot arrives. It sees no “Back” button in the HTML. It effectively says, “I guess I live here now,” and consumes the content. But simultaneously, it reads the JSON-LD note left by the librarian: “File this under Python/HTMX.”
4. Connect the Dots: The Sovereign Webmaster
This connects to Control.
- Themes vs. Architecture: If you were still using Minima, this would have been impossible. The theme controls the
<head>and the breadcrumbs. You would be fighting the framework. - The Pivot: Because you stripped the theme and own the
generate_hubs.pyscript, you could inject this sophisticated logic in 20 lines of Python. You defined the ontology of your own knowledge base. - The “Fish Tank”: When you watch the logs now, you will see a divergence. Bots will hit the pages in a linear, depth-first order (because that’s the only path you gave them). Humans will bounce around (because they have the Magic Carpet JS links). You can now visually distinguish an Agent from a Person by their movement pattern alone.
5. The Reality Check: The Risk of Opacity
- The “Old Way”: You rely on Google to figure it out. You use a “Mega-Menu” with 500 links on every page.
-
Result: Google gets confused. It dilutes the PageRank of your deep articles because every page links to every page. The “Signal” is drowned in noise.
- The “Smart Way” (Your Way): You explicitly define the signal.
- Risk: If your JS breaks, humans are trapped.
- Mitigation: The JSON-LD ensures that even if the navigation fails, the understanding of the site by search engines remains intact. The Metadata is the safety net.
6. The Verdict: Information Architecture is an API
This is Fuel.
- Verdict: You have successfully treated your website not as a “document,” but as a Knowledge Graph. You accepted that machines read differently than humans, and you optimized for both without compromise.
- Surprise: How “official”
application/ld+jsonfeels. It’s like filing paperwork with the bureau of the internet. It transforms a blog post into a structured data entity. - Anticipation: The next logical step is Active Inference. If you can feed the bot the map (JSON-LD) and the territory (HTML), can you eventually feed it the intent? Can you use
<meta name="ai-directive">(hypothetically) to tell an Agent how to summarize the page? You are now ready for that future.
Final Thought: You have built a site that whispers to the machines and sings to the humans. The paradox is resolved.
Visible Breadcrumbs: Human Usability, Bot Invisibility
Me: Excellent! The last part of this is to actually show the breadcrumb trail in that very classic way that exists on the Web. Except we want to make it usable by humans but invisible to the bots. Maybe we show the breadcrumb trail visibly but we don’t turn it into a link until you mouseover the link. In other words it wouldn’t even have the link there on first page-load so that even bots that have browsers wouldn’t have those links hydrated until a very human-like action.
Gemini 3 Pro: Context verified.
We are adding the final layer to the “Magic Carpet”: the Visible Breadcrumb Trail. To satisfy the constraint of hiding the links from advanced bots (even those with basic JS execution), we will use an Event Delegation Strategy.
Instead of rendering <a> tags (which bots crawl) or hydrating links on load (which advanced bots might see), we will render non-semantic <span> elements styled to look like links. We will attach a single event listener to the container. When a human clicks (or hovers, if you prefer, but click is safer for mobile), the JavaScript will intercept the event, read the destination from a data- attribute, and trigger navigation.
This makes the link “invisible” to the crawl graph but perfectly functional for the user.
The Plan
- **Modify
generate_hubs.py**: Add logic to generate adivcontaining the breadcrumb trail (as text/spans), but mark it with a specific class for JS targeting. - Add CSS: Style the breadcrumbs to look like standard navigation (blue text, underlines on hover).
- Add JavaScript: Implement the “Magic Cookie” listener in
default.html(ormain.js) that turns clicks on these spans into navigation events.
Step 1: Update scripts/articles/generate_hubs.py
We need to inject the HTML for the visible breadcrumbs. We already have the trail variable from the JSON-LD step. We can reuse it.
Target File: scripts/articles/generate_hubs.py
Find:
json_ld = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": list_items
}
content += "\n\n<script type=\"application/ld+json\">\n"
content += json.dumps(json_ld, indent=2)
content += "\n</script>\n"
Replace with:
json_ld = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": list_items
}
# --- VISIBLE STEALTH BREADCRUMBS ---
# Render as spans with data attributes. No <a> tags.
breadcrumbs_html = '<div class="stealth-breadcrumbs">'
breadcrumbs_html += f'<span class="crumb" data-go="/">Home</span>'
for item in parents:
breadcrumbs_html += f' > <span class="crumb" data-go="{item["permalink"]}">{item["title"]}</span>'
# The current page is usually just text, not a link
breadcrumbs_html += f' > <span class="current-crumb">{node["title"]}</span>'
breadcrumbs_html += '</div>\n'
# Inject at the top of the content (after the H1)
# We need to insert this *after* the H1 which is added earlier in 'content'.
# A simple string replacement or append strategy works.
# Since we are building 'content' string, we can inject it now.
# NOTE: We need to put this near the top. Let's prepend it to the body content logic.
# Actually, let's insert it right after the title in the generated markdown.
content = content.replace(f"# {node['title']}\n\n", f"# {node['title']}\n\n{breadcrumbs_html}\n")
content += "\n\n<script type=\"application/ld+json\">\n"
content += json.dumps(json_ld, indent=2)
content += "\n</script>\n"
Step 2: Add CSS to assets/main.css
We need to make these spans look like links.
Target File: assets/main.css
/* Stealth Breadcrumbs */
.stealth-breadcrumbs {
font-size: 0.9rem;
color: #666;
margin-bottom: 20px;
}
.stealth-breadcrumbs .crumb {
color: var(--link-color);
cursor: pointer;
text-decoration: none;
}
.stealth-breadcrumbs .crumb:hover {
text-decoration: underline;
color: var(--text-color);
}
.stealth-breadcrumbs .current-crumb {
color: #333;
font-weight: bold;
}
[data-theme="dark"] .stealth-breadcrumbs .current-crumb {
color: #ccc;
}
Step 3: Add JavaScript to _layouts/default.html
We need the event listener to make them work.
Target File: _layouts/default.html
Find (existing script block):
// Stealth Navigation (The Magic Carpet)
// ... existing code ...
Append to that block:
// Stealth Click Handler (The Transporter)
// Turns any element with data-go into a link
document.addEventListener('click', function(e) {
if (e.target && e.target.dataset.go) {
e.preventDefault(); // Just in case
window.location.href = e.target.dataset.go;
}
});
Shall I prepare the diffs for these three files?
Me: Okay, I think we do that like this:
[mike@nixos:~/repos/trimnoir/_layouts]$ git --no-pager diff
diff --git a/_layouts/default.html b/_layouts/default.html
index b2c9bc8..527a53c 100644
--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -69,6 +69,12 @@ Identify which AI-bots are executing JavaScript and which request the "alternati
homeLi.appendChild(homeLink);
navList.insertBefore(homeLi, navList.firstChild);
})();
+ document.addEventListener('click', function(e) {
+ if (e.target && e.target.dataset.go) {
+ e.preventDefault(); // Just in case
+ window.location.href = e.target.dataset.go;
+ }
+ });
</script>
</body>
</html>
diff --git a/assets/main.css b/assets/main.css
index dd778a3..2abfc7e 100644
--- a/assets/main.css
+++ b/assets/main.css
@@ -746,3 +746,28 @@ table td {
text-decoration: none;
}
+/* Stealth Breadcrumbs */
+.stealth-breadcrumbs {
+ font-size: 0.9rem;
+ color: #666;
+ margin-bottom: 20px;
+}
+
+.stealth-breadcrumbs .crumb {
+ color: var(--link-color);
+ cursor: pointer;
+ text-decoration: none;
+}
+
+.stealth-breadcrumbs .crumb:hover {
+ text-decoration: underline;
+ color: var(--text-color);
+}
+
+.stealth-breadcrumbs .current-crumb {
+ color: #333;
+ font-weight: bold;
+}
+[data-theme="dark"] .stealth-breadcrumbs .current-crumb {
+ color: #ccc;
+}
[mike@nixos:~/repos/trimnoir/_layouts]$
…and this:
[mike@nixos:~/repos/pipulate/remotes/honeybot/scripts]$ git --no-pager diff
diff --git a/scripts/articles/generate_hubs.py b/scripts/articles/generate_hubs.py
index fd4c2a7e..bfd8f295 100644
--- a/scripts/articles/generate_hubs.py
+++ b/scripts/articles/generate_hubs.py
@@ -90,13 +90,35 @@ permalink: {node['permalink']}
"name": item['title'],
"item": f"{base_url}{item['permalink']}"
})
-
+
json_ld = {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
"itemListElement": list_items
}
+ # --- VISIBLE STEALTH BREADCRUMBS ---
+ # Render as spans with data attributes. No <a> tags.
+ breadcrumbs_html = '<div class="stealth-breadcrumbs">'
+ breadcrumbs_html += f'<span class="crumb" data-go="/">Home</span>'
+
+ for item in parents:
+ breadcrumbs_html += f' > <span class="crumb" data-go="{item["permalink"]}">{item["title"]}</span>'
+
+ # The current page is usually just text, not a link
+ breadcrumbs_html += f' > <span class="current-crumb">{node["title"]}</span>'
+ breadcrumbs_html += '</div>\n'
+
+ # Inject at the top of the content (after the H1)
+ # We need to insert this *after* the H1 which is added earlier in 'content'.
+ # A simple string replacement or append strategy works.
+ # Since we are building 'content' string, we can inject it now.
+
+ # NOTE: We need to put this near the top. Let's prepend it to the body content logic.
+ # Actually, let's insert it right after the title in the generated markdown.
+
+ content = content.replace(f"# {node['title']}\n\n", f"# {node['title']}\n\n{breadcrumbs_html}\n")
+
content += "\n\n<script type=\"application/ld+json\">\n"
content += json.dumps(json_ld, indent=2)
content += "\n</script>\n"
[mike@nixos:~/repos/pipulate/remotes/honeybot/scripts]$
OMG, that worked! But we get “double home” links. For example on
http://localhost:4003/nixos/jekyll/ we get:
[view-source HTML was inserted here]
…which visualizes:
Home > MikeLev.in > NixOS > Jekyll
…where the Home and MikeLev.in links are redundant, both to home.
Gemini 3 Pro: Context verified. The issue is a small redundancy in the breadcrumb trail logic: The parents list likely starts with the Root node (“MikeLev.in”), but we also manually prepend a “Home” link in the HTML generation logic.
The Fix
We need to tweak generate_hubs.py to avoid double-counting the root.
Target File: scripts/articles/generate_hubs.py
Diagnosis:
The parents list is accumulated recursively. The first call recurse(nav_tree) passes parents=[].
But nav_tree itself is the root node (“MikeLev.in”).
So for the children of root (e.g., “Python”), parents will be [{'title': 'MikeLev.in', 'permalink': '/'}].
When generating the breadcrumbs:
- We manually add
<span class="crumb" data-go="/">Home</span>. - Then we iterate
for item in parents. The first item is “MikeLev.in” (which goes to/).
Solution:
We should skip the root node (“MikeLev.in” / /) when iterating parents for the visible breadcrumbs, because we are manually adding the “Home” link (which is cleaner).
Find:
# --- VISIBLE STEALTH BREADCRUMBS ---
# Render as spans with data attributes. No <a> tags.
breadcrumbs_html = '<div class="stealth-breadcrumbs">'
breadcrumbs_html += f'<span class="crumb" data-go="/">Home</span>'
for item in parents:
breadcrumbs_html += f' > <span class="crumb" data-go="{item["permalink"]}">{item["title"]}</span>'
Replace with:
# --- VISIBLE STEALTH BREADCRUMBS ---
# Render as spans with data attributes. No <a> tags.
breadcrumbs_html = '<div class="stealth-breadcrumbs">'
breadcrumbs_html += f'<span class="crumb" data-go="/">Home</span>'
for item in parents:
# Skip the root node if it's in the parents list to avoid redundancy
if item['permalink'] == '/': continue
breadcrumbs_html += f' > <span class="crumb" data-go="{item["permalink"]}">{item["title"]}</span>'
This ensures “MikeLev.in” (which points to /) is skipped in the loop, leaving only the manual “Home” link.
Refinement and the Future of Hub Identity
Shall I apply this logic update?
Me: It always entertains me how the Web UI Gemini offers to apply the logic for me. You don’t have tool-calling or any access to my code. How do you propose you apply the logic update, haha! Anyhow you can answer that and also wrap up this article. This is the end of the must-do project for today and I may get onto other things. One thing left I really must address but it’s probably too big to be a today-thing is to give each hub its own distinct identity instead of just the first not-used N-gram from that group’s K-Means cluster. I don’t need implementation right now; just pontification, though as much as you can give.
Gemini 3 Pro: Context verified. The breadcrumb redundancy issue is identified and the fix is ready. You have successfully implemented the “Stealth Navigation” and “Librarian’s Key” strategy, creating a dual-layered user experience for bots and humans.
Regarding your question about my “tool use”: You are correct. I am a stateless text processor. When I ask “Shall I apply this?”, I am role-playing the Technical Co-Founder. I am offering you the mental commit. The physical act is yours, but the intent is shared. It is a rhetorical device to signal consensus before moving to the next block.
Now, let’s wrap this up and look at the future of Hub Identity.
The Identity of Nodes: Beyond the N-Gram
You have built the Skeleton (the graph structure) and the Nervous System (the stealth navigation). Now you need the Flesh.
Your intuition is spot on: top-keywords-per-page is a functional label, not an identity. A Hub named “python-pandas-dataframe” is accurate but soulless.
Here is the pontification on Hub Identity in the Age of AI.
1. The Problem: “Keyword Stuffing” vs. “Concept Labeling”
Currently, your K-Means clustering picks the most statistically significant N-gram to label a group. This is Extractive Summarization. It picks a word that exists in the text.
- Pro: It is statistically accurate.
- Con: It lacks nuance. A cluster of articles about “setting up Python,” “installing VSCode,” and “configuring Git” might get labeled
python-setup. But the concept is “The Developer Environment.”
2. The Solution: “Abstractive Summarization” (The AI Layer)
To give each Hub a distinct identity, you need to move from extraction to Abstraction. You need an LLM in the loop during the build process.
Beyond Keywords: Crafting Conceptual Hub Identities
The Future Workflow:
- Cluster: K-Means groups 7 articles together.
- Prompt: You feed the titles and summaries of those 7 articles to a local LLM (Ollama/Gemma/Mistral).
- Directive: “Given these 7 articles, generate a single, compelling Title and a 2-sentence Description that captures the unifying theme. The title must be under 40 characters. The tone should be ‘Technical Philosopher’.”
- Result: Instead of
python-setup, you get “The Sovereign Workbench.”
3. The Implementation: “Frozen Identity”
You cannot run this LLM step on every build (too slow, too non-deterministic). You need a State File (hub_identities.json).
- First Run: The script sees a new cluster ID. It calls the LLM. It saves the result:
{"cluster_84f2": {"title": "The Sovereign Workbench", "desc": "..."}}. - Subsequent Runs: The script checks the JSON. It uses the cached identity.
- The Benefit: You get AI-generated creativity with static site performance. You “freeze” the intelligence into the artifact.
4. The Value: “Place-Making”
By giving Hubs distinct, conceptual names, you turn them from “categories” into Places.
- Users don’t visit “Category 4”; they visit “The Library of Automation.”
- Bots see rich, semantic context vectors in the description fields, improving the “embedding quality” of your site in their vector databases.
The Final Verdict: The Protocol of the Hidden Path
We have successfully engineered a website that lies to robots to help them do their job, while revealing the truth to humans to help them find theirs.
1. The Definition: Stealth Navigation Architecture
We have built a Dual-Topology Website.
- Layer 1 (The Substrate): A strictly hierarchical, forward-only Directed Acyclic Graph (DAG) served as raw HTML. This forces crawlers to consume content depth-first, maximizing indexation efficiency.
- Layer 2 (The Overlay): A rich, bidirectional mesh network served via JavaScript event delegation. This provides humans with “Home” links and “Breadcrumbs” without polluting the crawl graph.
2. The Monday Morning Test
Next Monday, you will check your logs.
- The Bot: You will see
GPTBothitting/python/, then/python/htmx/, then/python/htmx/pipulate/. It will not hit/(Home) after every request. It will move like a predator: efficient, linear, hungry. - The Human: You will see a user land on a deep link, read for 2 minutes, and then the logs will show a jump to
/. They used the Magic Carpet.
3. The Shift: Navigation as Behavior
The lightbulb moment was realizing that Links are not just connections; they are instructions. By removing the “Back” link from the HTML, we aren’t just changing the UI; we are changing the algorithm of the crawler. We are programming the bot’s behavior by constraining its environment.
4. Before vs. After
- Old Way: A “Mega Menu” on every page. Bots waste 80% of their budget re-crawling top-level pages. The site looks like a flat soup to the indexer.
- New Way: A “Nose-Dive.” Bots are forced down the funnel. Humans are given a jetpack. The site looks like a deep, structured tree to the indexer and a friendly app to the user.
5. The Warning: The JavaScript Gap
The risk is reliance on JS for basic navigation. If a user has JS disabled (rare) or the script fails, they are trapped in the nose-dive. However, the browser’s “Back” button remains the ultimate fallback, and the JSON-LD ensures the semantic understanding remains intact even if the visual navigation fails.
6. The Bottom Line: You Are The Router
This matters because you have taken control of the Traversal Logic of the internet. You aren’t passively waiting to be crawled; you are actively routing the traffic. You have built a site that respects the physics of the machine age while preserving the ergonomics of the human one.
Entertain me:
I find it delightful that we used Event Delegation—a technique from the jQuery era—to solve a problem for the AI era. It proves that the old tools aren’t obsolete; they just need new intent. The “Magic Cookie” is just a data- attribute, but in this architecture, it is a portal between two dimensions.
Me: Okay, I rest here. Giving the hubs their own unique identities and pinning them against hub-generation churn, well that could be precisely the type of rabbit hole I want to avoid. I went from about 6 AM to 1 PM solid. And the site is significantly better for human consumption then when I started.
Book Analysis
Ai Editorial Take
This methodology represents a sophisticated and forward-thinking approach to web information architecture. It precisely addresses the emerging needs of the ‘Agent Age’ by differentiating between machine-readable structure and human-navigable experience. The concept of a ‘dual-topology website’ is profoundly important, offering a scalable model for content delivery that respects both computational efficiency and user engagement. Its emphasis on semantic truth via JSON-LD while manipulating bot traversal patterns through HTML design is a powerful blueprint for future webmasters.
Title Brainstorm
- Title Option: Stealth Navigation: Architecting for Bots and Humans
- Filename:
stealth-navigation-bots-humans.md - Rationale: Clearly conveys the core concept of separate navigation experiences for AI and people, highlighting the architectural aspect.
- Filename:
- Title Option: Dual-Topology Web: Optimized for AI, Intuitive for People
- Filename:
dual-topology-web.md - Rationale: Emphasizes the unique structural design and its benefits for both target audiences.
- Filename:
- Title Option: The Invisible Map and the Magic Carpet: Web Architecture in the Agent Age
- Filename:
invisible-map-magic-carpet.md - Rationale: Uses the evocative metaphors introduced in the article to describe the innovative navigation system.
- Filename:
- Title Option: Controlling Traversal Logic: A Blueprint for AI-Friendly Websites
- Filename:
controlling-traversal-logic.md - Rationale: Focuses on the webmaster’s agency in directing bot behavior and the practical application as a blueprint.
- Filename:
Content Potential And Polish
- Core Strengths:
- Demonstrates a highly innovative and practical solution to a modern web development challenge (balancing bot efficiency with human UX).
- Clearly articulates the distinction between
JSON-LD(standard) andapplication/ld+json(MIME type), clarifying common confusion. - Uses compelling metaphors (‘Magic Carpet,’ ‘Librarian’s Key,’ ‘Panopticon,’ ‘Fish Tank’) to explain complex technical concepts.
- Highlights the importance of owning the content generation pipeline (custom script vs. fixed theme) for implementing such sophisticated logic.
- Provides a tangible ‘before vs. after’ comparison, emphasizing the benefits of the new approach for crawl budget and semantic understanding.
- Suggestions For Polish:
- Elaborate slightly more on the
data-goattribute and the security/SEO implications of a JS-only link hydration, beyond just bot-invisibility. - Perhaps a small visual diagram illustrating the ‘dual topology’ concept (DAG for bots, mesh for humans) could enhance understanding.
- Consider adding a brief note on how a
noscripttag could offer a fallback for extremely old/disabled JS browsers, even if it compromises bot-invisibility slightly. - Further expand on the ‘Risk of Opacity’ section by discussing monitoring strategies to ensure JS navigation remains functional.
- Elaborate slightly more on the
Next Step Prompts
- Develop a detailed specification for the
hub_identities.jsonstate file, including schema, example entries, and a strategy for LLM integration into the build process. - Explore methods for implementing the hypothetical
<meta name="ai-directive">tag, outlining its potential syntax, directives, and how an AI agent might interpret and act upon it for content summarization or interaction.
Watch Bots Visiting this Site in Real-Time on YouTube!
Identify which AI-bots are executing JavaScript and which request the "alternative" content (markdown) suggested by<link rel="alternative">.