Tendril Knowledge Graph
Cytoscape.js-based knowledge graph for visualizing content relationships across Arcturus-Prime
Tendril Knowledge Graph
Tendril is the knowledge graph system that powers content discovery on Arcturus-Prime. It takes the metadata from every blog post, journal entry, and documentation page — tags, categories, related posts — and renders an interactive, physics-based graph that shows how everything connects. Click a node, see what it links to, find related content you didn’t know existed.
Package Location
Tendril lives in the monorepo as a local package:
packages/tendril-graph/
├── src/
│ ├── graph-builder.js # Builds graph data from content metadata
│ ├── graph-renderer.js # Cytoscape.js rendering logic
│ ├── layout-engine.js # Physics simulation parameters
│ └── types.ts # TypeScript type definitions
├── package.json # @tendril/graph
└── README.md
The package is referenced as @tendril/graph in the main Arcturus-Prime site’s package.json via workspace linking. No npm publishing — it’s a local dependency that builds with the rest of the site.
How It Works
Data Pipeline
Tendril builds its graph at build time from content metadata. The pipeline:
- Collect: At build time, Tendril scans all content collections (posts, journal, docs) and extracts frontmatter fields:
title,tags,category,related_posts,pubDate - Map: Each piece of content becomes a node. Tags become shared nodes that connect related content.
related_postsfrontmatter creates explicit edges between specific posts - Score: Edges are weighted by connection type — explicit
related_postslinks have higher weight than shared-tag connections, and multiple shared tags increase edge weight - Output: The graph data is serialized as JSON and embedded in the page, ready for Cytoscape.js to render client-side
Node Types
| Node Type | Represents | Visual |
|---|---|---|
post | Blog post | Large circle, colored by category |
journal | Journal entry | Medium circle, muted tone |
tag | Content tag | Small diamond, accent color |
category | Content category | Medium hexagon, category color |
doc | Documentation page | Small square, neutral |
Edge Types
| Edge Type | Connects | Weight |
|---|---|---|
related | Two posts linked via related_posts | High (explicit connection) |
tagged | Post/journal to tag | Medium |
categorized | Post to category | Medium-high |
co-tagged | Two posts sharing 2+ tags | Low-medium (inferred) |
Components
Tendril provides two Astro components with different use cases:
KnowledgeGraph.astro (Full Interactive)
The full knowledge graph is used on the /graph page and provides the complete interactive experience:
---
import KnowledgeGraph from '@tendril/graph/KnowledgeGraph.astro';
---
<KnowledgeGraph
width="100%"
height="600px"
focusNode={null}
showControls={true}
enablePhysics={true}
/>
Features:
- Full graph with all nodes and edges
- Pan, zoom, drag interactions
- Click a node to highlight its connections and dim everything else
- Search/filter by tag, category, or content type
- Physics simulation that organically clusters related content
- Control panel for adjusting physics parameters (spring strength, repulsion, damping)
- Responsive — scales to container width
The full graph can get dense with hundreds of posts. Tendril handles this with progressive disclosure — on load, it shows top-level clusters (categories and high-weight tags) and expands detail as you zoom in or click into a cluster.
MiniGraph.astro (Compact Sidebar)
The mini graph is designed for blog post pages, showing a compact view of the current post’s immediate neighborhood:
---
import MiniGraph from '@tendril/graph/MiniGraph.astro';
---
<MiniGraph
currentSlug={post.slug}
depth={2}
width="300px"
height="250px"
/>
Features:
- Shows only the current post and its connections up to
depthlevels - No controls panel — just the graph
- Click a related node to navigate to that post
- Compact enough for a sidebar or post footer
- Same physics engine as the full graph, but with tighter parameters for small spaces
The depth parameter controls how far the graph extends from the current post. At depth=1, you see the current post, its tags, and posts that share those tags. At depth=2, you see the next ring of connections — tags of those related posts and their connections. Beyond depth 2, the mini graph gets too crowded to be useful.
Cytoscape.js
Tendril is built on Cytoscape.js, a graph theory library for visualization and analysis. Cytoscape handles rendering, layout, user interaction, and graph algorithms. Tendril provides the data layer and Arcturus-Prime-specific customization on top.
Why Cytoscape.js?
- Battle-tested: Used in bioinformatics and network science for years. It handles thousands of nodes without choking
- Layout algorithms: Built-in support for force-directed, hierarchical, grid, and custom layouts
- Extensible: Cytoscape has a plugin ecosystem for additional layouts, analysis algorithms, and rendering options
- Canvas-based: Renders on HTML5 Canvas, not SVG. This matters at scale — SVG DOM elements get slow with hundreds of nodes, Canvas doesn’t
- Headless mode: Can run without a DOM for server-side graph analysis (used during build for scoring)
Alternatives Considered
D3.js was the other candidate. D3 is more flexible (you build everything from primitives) but that flexibility means more code for basic graph operations. Cytoscape gives you a graph-aware API out of the box — adding a node, querying neighbors, running BFS, applying layouts — without building it all from scratch. For a knowledge graph, the graph-first API won.
Physics-Based Layout
The graph uses a force-directed layout where nodes repel each other (like charged particles) and edges pull connected nodes together (like springs). The result is an organic layout where clusters of related content naturally group together.
Tunable Parameters
| Parameter | Default | Effect |
|---|---|---|
springLength | 120 | Rest length of edge springs. Longer = more spread out |
springCoefficient | 0.0008 | Spring stiffness. Higher = edges snap together faster |
gravity | -1.2 | Global repulsion between all nodes. More negative = more spread |
dragCoefficient | 0.02 | Friction. Higher = settles faster but less organic |
theta | 0.8 | Barnes-Hut approximation. Higher = faster but less accurate |
timeStep | 0.5 | Simulation speed. Lower = smoother animation |
These are exposed in the full KnowledgeGraph’s control panel for live adjustment. The defaults produce a layout that’s readable for a graph of ~200-500 nodes. For larger graphs, increasing gravity (more negative) and springLength spreads things out enough to remain navigable.
Stabilization
The physics simulation runs until the graph stabilizes — meaning the total kinetic energy of all nodes drops below a threshold. On initial load, this takes 2-3 seconds for a medium-sized graph. During this time, nodes are visibly settling into their positions, which actually looks good as an entry animation.
Once stabilized, the simulation pauses to save CPU. It reactivates when you drag a node or apply a filter, then re-stabilizes.
Integration with Arcturus-Prime
Where It Appears
- /graph: Full-page knowledge graph with controls and search
- Blog post pages: MiniGraph in the sidebar showing related content
- Documentation pages: Optional MiniGraph showing doc-to-doc relationships
Content Metadata Requirements
For Tendril to include content in the graph, the frontmatter needs at minimum:
---
title: "Post Title"
tags:
- at-least-one-tag
---
Optional fields that enrich the graph:
---
category: "homelab"
related_posts:
- "2026-01-15-build-swarm-launch"
- "2026-02-01-argo-os-part-5"
---
Posts without tags are isolated nodes — they appear in the graph but have no connections. Posts with related_posts links get explicit high-weight edges that pull them close together in the physics layout.
Build-Time Processing
Tendril’s graph data is generated during npm run build (or astro build). The build step:
- Reads all content collections via Astro’s content API
- Runs the graph builder to create nodes and edges
- Serializes the graph as a JSON blob
- Injects the JSON into KnowledgeGraph and MiniGraph components as props
There’s no runtime graph computation. The graph data is static once the site builds. This means adding a new post requires a rebuild for the graph to include it, but it also means the graph loads instantly for visitors — no API calls, no loading spinners, just a pre-computed data structure ready to render.
Performance
On a graph with ~300 nodes and ~1200 edges (typical for Arcturus-Prime with all posts, journals, tags, and categories):
- Build time: ~2 seconds to compute graph data
- Bundle size: ~45KB for graph JSON data, ~85KB for Cytoscape.js (gzipped)
- Render time: ~500ms to initial paint, ~2.5 seconds to full physics stabilization
- Memory: ~15MB peak during physics simulation
The MiniGraph is much lighter — typically 10-20 nodes and 15-30 edges per post, rendering in under 200ms with negligible memory impact.
Development
Working on Tendril
cd ~/Development/Arcturus-Prime
# The package is linked via workspaces
# Changes to packages/tendril-graph/ are picked up immediately
npm run dev
# Visit http://localhost:4321/graph to see the full graph
Cytoscape.js loads client-side only (it needs the DOM for Canvas rendering), so the Astro components use client:load or client:visible directives to control when the JavaScript bundles ship.
Adding New Node Types
To add a new content type to the graph (e.g., a “project” node type), update the graph builder in packages/tendril-graph/src/graph-builder.js to include the new collection, define its node style in the renderer, and rebuild.