diff --git a/src/components/KnowledgeGraph.astro b/src/components/KnowledgeGraph.astro
index 81fb162..c0e6eb1 100644
--- a/src/components/KnowledgeGraph.astro
+++ b/src/components/KnowledgeGraph.astro
@@ -9,6 +9,7 @@ export interface GraphNode {
category?: string;
tags?: string[];
url?: string; // URL for linking
+ content?: string; // Add content property for post full content
}
export interface GraphEdge {
@@ -26,12 +27,10 @@ export interface GraphData {
interface Props {
graphData: GraphData;
height?: string; // e.g., '500px'
- width?: string; // e.g., '100%'
initialFilter?: string; // Optional initial filter
- isMinimal?: boolean; // For smaller/simpler graphs inline in posts
}
-const { graphData, height = "400px", width = "100%", initialFilter = "all", isMinimal = false } = Astro.props;
+const { graphData, height = "50vh", initialFilter = "all" } = Astro.props;
// Generate colors based on node types
const nodeTypeColors = {
@@ -41,7 +40,7 @@ const nodeTypeColors = {
};
// Generate predefined colors for categories
-const predefinedColors = {
+const predefinedColors = {
'Kubernetes': '#326CE5', 'Docker': '#2496ED', 'DevOps': '#FF6F61',
'Homelab': '#06B6D4', 'Networking': '#9333EA', 'Infrastructure': '#10B981',
'Automation': '#F59E0B', 'Security': '#EF4444', 'Monitoring': '#6366F1',
@@ -73,160 +72,36 @@ const nodeTypeCounts = {
tag: graphData.nodes.filter(node => node.type === 'tag').length,
category: graphData.nodes.filter(node => node.type === 'category').length
};
-
-// Determine layout settings based on minimal mode
-const layoutSettings = isMinimal ? {
- name: 'cose',
- idealEdgeLength: 60,
- nodeOverlap: 20,
- refresh: 20,
- fit: true,
- padding: 20,
- randomize: false,
- componentSpacing: 60,
- nodeRepulsion: 800000,
- edgeElasticity: 150,
- nestingFactor: 7,
- gravity: 25,
- numIter: 1500,
- initialTemp: 200,
- coolingFactor: 0.95,
- minTemp: 1.0,
- animate: true,
- animationDuration: 800
-} : {
- name: 'cose',
- idealEdgeLength: 75,
- nodeOverlap: 30,
- refresh: 20,
- fit: true,
- padding: 30,
- randomize: false,
- componentSpacing: 60,
- nodeRepulsion: 1000000,
- edgeElasticity: 150,
- nestingFactor: 7,
- gravity: 30,
- numIter: 2000,
- initialTemp: 250,
- coolingFactor: 0.95,
- minTemp: 1.0,
- animate: true,
- animationDuration: 800
-};
-
-// Default physics settings
-const defaultPhysics = {
- nodeRepulsion: 9000,
- edgeElasticity: 50,
- gravity: 10,
- linkDistance: 100,
- nodeSize: 1.0,
- linkThickness: 1.0
-};
---
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Graph Settings
-
-
-
Display
-
-
-
Node Size: 1.0
-
+
+
+
-
-
-
-
- Apply
- Reset
-
+
-
- {!isMinimal && (
-
-
- How to use the Knowledge Graph
-
-
This Knowledge Graph visualizes connections between blog content:
-
- Posts - Blog articles (circle nodes)
- Tags - Content topics (diamond nodes)
-
-
Interactions:
-
- Click a node to see its connections and details
- Click a tag node to filter posts by that tag
- Click a post node to highlight that specific post
- Use mouse wheel to zoom in/out and drag to pan
- Click an empty area to reset the view
-
-
-
-
- )}
-
-
- {!isMinimal && (
-
-
-
All
-
Posts ({nodeTypeCounts.post})
-
Tags ({nodeTypeCounts.tag})
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
Select a post node to view its content
+
+
+
+
+
+
+
+
+ All
+ Posts ({nodeTypeCounts.post})
+ Tags ({nodeTypeCounts.tag})
+
+
-
- )}
-
-
-
-
-
-
-
-
-
- Exit Fullscreen
-
+
+
-
-{isMinimal && (
-
-)}
-
-
\ No newline at end of file
diff --git a/src/components/MiniGraph.astro b/src/components/MiniGraph.astro
new file mode 100644
index 0000000..28eaa7a
--- /dev/null
+++ b/src/components/MiniGraph.astro
@@ -0,0 +1,222 @@
+---
+// MiniGraph.astro - A standalone mini knowledge graph component
+// This component is designed to work independently from the blog structure
+
+// Define props interface
+interface Props {
+ slug: string; // Current post slug
+ title: string; // Current post title
+ tags?: string[]; // Current post tags
+ category?: string; // Current post category
+}
+
+// Extract props with defaults
+const {
+ slug,
+ title,
+ tags = [],
+ category = "Uncategorized"
+} = Astro.props;
+
+// Generate unique ID for the graph container
+const graphId = `graph-${Math.random().toString(36).substring(2, 8)}`;
+
+// Prepare simple graph data for just the post and its tags
+const nodes = [
+ // Current post node
+ {
+ id: slug,
+ label: title,
+ type: "post"
+ },
+ // Tag nodes
+ ...tags.map(tag => ({
+ id: `tag-${tag}`,
+ label: tag,
+ type: "tag"
+ }))
+];
+
+// Create edges connecting post to tags
+const edges = tags.map(tag => ({
+ source: slug,
+ target: `tag-${tag}`,
+ type: "post-tag"
+}));
+
+// Prepare graph data object
+const graphData = { nodes, edges };
+---
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/components/MiniKnowledgeGraph.astro b/src/components/MiniKnowledgeGraph.astro
index dd4bded..bc49e1f 100644
--- a/src/components/MiniKnowledgeGraph.astro
+++ b/src/components/MiniKnowledgeGraph.astro
@@ -1,1752 +1,341 @@
---
-// src/components/MiniKnowledgeGraph.astro
-// Enhanced smaller version of the Knowledge Graph for blog posts with Obsidian-like physics
+// MiniKnowledgeGraph.astro - Inline version that replaces the Tags section
+// Designed to work within the existing sidebar structure
export interface GraphNode {
id: string;
label: string;
- type: 'post' | 'tag' | 'category' | 'current';
- category?: string;
- tags?: string[];
+ type: 'post' | 'tag' | 'category';
url?: string;
}
export interface GraphEdge {
source: string;
target: string;
- type: 'post-tag' | 'post-category' | 'post-post';
- strength?: number;
-}
-
-export interface GraphData {
- nodes: GraphNode[];
- edges: GraphEdge[];
+ type: 'post-tag' | 'post-post';
}
interface Props {
- currentPost: any; // Current post data
- relatedPosts?: any[]; // Optional related posts array
- height?: string;
+ currentPost: any;
+ relatedPosts?: any[];
}
-const { currentPost, relatedPosts = [], height = "250px" } = Astro.props;
+const { currentPost, relatedPosts = [] } = Astro.props;
-// Prepare tags as nodes
-const tagNodes = (currentPost.data.tags || []).map(tag => ({
- id: `tag-${tag}`,
- label: tag,
- type: 'tag',
- url: `/tag/${tag}/`
-}));
+// Generate unique ID for the graph container
+const graphId = `mini-cy-${Math.random().toString(36).substring(2, 9)}`;
-// Create edges from the current post to its tags
-const currentPostTagEdges = (currentPost.data.tags || []).map(tag => ({
- source: currentPost.slug,
- target: `tag-${tag}`,
- type: 'post-tag',
- strength: 2 // Stronger connection for current post
-}));
-
-// Create the current post node with a special type
-const currentPostNode = {
- id: currentPost.slug,
- label: currentPost.data.title,
- type: 'current', // Special type for styling
- category: currentPost.data.category || 'Uncategorized',
- tags: currentPost.data.tags || [],
- url: `/posts/${currentPost.slug}/`
+// Ensure currentPost has necessary properties
+const safeCurrentPost = {
+ id: currentPost.slug || 'current-post',
+ title: currentPost.data?.title || 'Current Post',
+ tags: currentPost.data?.tags || [],
+ category: currentPost.data?.category || 'Uncategorized',
};
-// Process related posts
-const relatedPostNodes = relatedPosts.map(post => ({
- id: post.slug,
- label: post.data.title,
- type: 'post',
- category: post.data.category || 'Uncategorized',
- tags: post.data.tags || [],
- url: `/posts/${post.slug}/`
-}));
+// Prepare graph data
+const nodes: GraphNode[] = [];
+const edges: GraphEdge[] = [];
+const addedTagIds = new Set
();
+const addedPostIds = new Set();
-// Create edges from related posts to their tags that are also current post tags
-// This ensures we only show connections relevant to the current post
-const relatedPostTagEdges = [];
-relatedPosts.forEach(post => {
- // For each tag that is also in current post, create an edge
- (post.data.tags || []).filter(tag =>
- (currentPost.data.tags || []).includes(tag)
- ).forEach(tag => {
- relatedPostTagEdges.push({
- source: post.slug,
- target: `tag-${tag}`,
- type: 'post-tag',
- strength: 1
+// Add current post node
+nodes.push({
+ id: safeCurrentPost.id,
+ label: safeCurrentPost.title,
+ type: 'post',
+ url: `/posts/${safeCurrentPost.id}/`
+});
+addedPostIds.add(safeCurrentPost.id);
+
+// Add tags from current post
+safeCurrentPost.tags.forEach((tag: string) => {
+ const tagId = `tag-${tag}`;
+
+ // Only add if not already added
+ if (!addedTagIds.has(tagId)) {
+ nodes.push({
+ id: tagId,
+ label: tag,
+ type: 'tag',
+ url: `/tag/${tag}/`
+ });
+ addedTagIds.add(tagId);
+ }
+
+ // Add edge from current post to tag
+ edges.push({
+ source: safeCurrentPost.id,
+ target: tagId,
+ type: 'post-tag'
+ });
+});
+
+// Add related posts and their connections
+if (relatedPosts && relatedPosts.length > 0) {
+ relatedPosts.forEach(post => {
+ if (!post) return;
+
+ const postId = post.slug || `post-${Math.random().toString(36).substring(2, 9)}`;
+
+ // Skip if already added or is the current post
+ if (addedPostIds.has(postId) || postId === safeCurrentPost.id) {
+ return;
+ }
+
+ // Add related post node
+ nodes.push({
+ id: postId,
+ label: post.data?.title || 'Related Post',
+ type: 'post',
+ url: `/posts/${postId}/`
+ });
+ addedPostIds.add(postId);
+
+ // Add edge from current post to related post
+ edges.push({
+ source: safeCurrentPost.id,
+ target: postId,
+ type: 'post-post'
+ });
+
+ // Add shared tags and their connections
+ const postTags = post.data?.tags || [];
+ postTags.forEach((tag: string) => {
+ // Only add connections for tags that the current post also has
+ if (safeCurrentPost.tags.includes(tag)) {
+ const tagId = `tag-${tag}`;
+
+ // Add edge from related post to shared tag
+ edges.push({
+ source: postId,
+ target: tagId,
+ type: 'post-tag'
+ });
+ }
});
});
-
- // Also create edges between related posts and current post
- relatedPostTagEdges.push({
- source: currentPost.slug,
- target: post.slug,
- type: 'post-post',
- strength: 1
- });
-});
+}
-// Combine all nodes and edges
-const graphData = {
- nodes: [currentPostNode, ...tagNodes, ...relatedPostNodes],
- edges: [...currentPostTagEdges, ...relatedPostTagEdges]
-};
-
-// Define node colors, radii and edge colors
-const nodeColors = {
- currentArticle: "#FF5733",
- relatedArticle: "#3366CC",
- tag: "#33CC66",
- category: "#9966CC" // Add category color
-};
-
-const nodeRadii = {
- currentArticle: 20,
- relatedArticle: 15,
- tag: 12,
- category: 18 // Add category size
-};
-
-// Calculate node sizes
-const nodeSizes = {};
-// Current post should be largest
-nodeSizes[currentPost.slug] = 25;
-// Tag sizes are middle
-tagNodes.forEach(node => {
- nodeSizes[node.id] = 18;
-});
-// Related posts are smaller
-relatedPostNodes.forEach(node => {
- nodeSizes[node.id] = 20;
-});
-
-// Default physics settings with Obsidian-like behavior
-const defaultPhysics = {
- nodeRepulsion: 7500, // Increased to prevent nodes from getting too close
- edgeElasticity: 0.35, // More flexible edges like Obsidian
- gravity: 0.4, // Light gravity to keep nodes centered
- animate: true,
- damping: 0.12, // Mimics Obsidian's smooth drag effect
- pullStrength: 0.09 // Strength of pull effect when dragging nodes
-};
+// Generate graph data
+const graphData = { nodes, edges };
---
-
-
-
-
-
-
{title || 'Article Connections'}
-
-
-
-
-
-
-
-
-
-
-
-
- Repulsion: 7500
-
-
-
- Elasticity: 0.35
-
-
-
- Gravity: 0.4
-
-
-
- Reset
- Apply
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Current Article
-
-
-
- Related Articles
-
-
-
- Category
-
-
-
- Tags
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Select a post in the graph to view its content
-
-
-
-
+
-
-
-
\ No newline at end of file
diff --git a/src/layouts/BlogPostLayout.astro b/src/layouts/BlogPostLayout.astro
index c3de0db..3082559 100644
--- a/src/layouts/BlogPostLayout.astro
+++ b/src/layouts/BlogPostLayout.astro
@@ -2,8 +2,7 @@
import BaseLayout from './BaseLayout.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
-import Newsletter from '../components/Newsletter.astro';
-import MiniKnowledgeGraph from '../components/MiniKnowledgeGraph.astro';
+import MiniKnowledgeGraph from '../components/MiniKnowledgeGraph.astro'; // Restore original or keep if needed
import { getCollection } from 'astro:content';
interface Props {
@@ -18,16 +17,68 @@ interface Props {
readTime?: string;
draft?: boolean;
author?: string;
- github?: string;
- live?: string;
- technologies?: string[];
- related_posts?: string[]; // Explicit related posts by slug
- }
+ // Field for explicitly related posts
+ related_posts?: string[];
+ },
+ slug: string // Add slug to props
}
-const { frontmatter } = Astro.props;
+const { frontmatter, slug } = Astro.props;
-// Format dates
+// Get all posts for finding related content
+const allPosts = await getCollection('posts');
+
+// Create a currentPost object that matches the structure expected by MiniKnowledgeGraph
+const currentPost = {
+ slug: slug,
+ data: frontmatter
+};
+
+// Find related posts - first from explicitly defined related_posts
+const explicitRelatedPosts = frontmatter.related_posts
+ ? allPosts.filter(post =>
+ frontmatter.related_posts?.includes(post.slug) &&
+ post.slug !== slug
+ )
+ : [];
+
+// Then find posts with shared tags (if we need more related posts)
+const MAX_RELATED_POSTS = 3;
+let relatedPostsByTags = [];
+
+if (explicitRelatedPosts.length < MAX_RELATED_POSTS && frontmatter.tags && frontmatter.tags.length > 0) {
+ // Create a map of posts by tags for efficient lookup
+ const postsByTag = new Map();
+ frontmatter.tags.forEach(tag => {
+ postsByTag.set(tag, allPosts.filter(post =>
+ post.slug !== slug &&
+ post.data.tags?.includes(tag) &&
+ !explicitRelatedPosts.some(p => p.slug === post.slug)
+ ));
+ });
+
+ // Score posts by number of shared tags
+ const scoredPosts = new Map();
+
+ postsByTag.forEach((posts, tag) => {
+ posts.forEach(post => {
+ const currentScore = scoredPosts.get(post.slug) || 0;
+ scoredPosts.set(post.slug, currentScore + 1);
+ });
+ });
+
+ // Convert to array, sort by score, and take what we need
+ relatedPostsByTags = Array.from(scoredPosts.entries())
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, MAX_RELATED_POSTS - explicitRelatedPosts.length)
+ .map(([slug]) => allPosts.find(post => post.slug === slug))
+ .filter(Boolean); // Remove any undefined entries
+}
+
+// Combine explicit and tag-based related posts
+const relatedPosts = [...explicitRelatedPosts, ...relatedPostsByTags];
+
+// Format date
const formattedPubDate = frontmatter.pubDate ? new Date(frontmatter.pubDate).toLocaleDateString('en-us', {
year: 'numeric',
month: 'long',
@@ -42,58 +93,6 @@ const formattedUpdatedDate = frontmatter.updatedDate ? new Date(frontmatter.upda
// Default image if heroImage is missing
const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg';
-
-// Get related posts for MiniKnowledgeGraph
-// First get all posts
-const allPosts = await getCollection('posts').catch(() => []);
-
-// Find the current post in collection
-const currentPost = allPosts.find(post =>
- post.data.title === frontmatter.title ||
- post.slug === frontmatter.title.toLowerCase().replace(/\s+/g, '-')
-);
-
-// Get related posts - first from explicit frontmatter relation, then by tag similarity
-let relatedPosts = [];
-
-// If related_posts is specified in frontmatter, use those first
-if (frontmatter.related_posts && frontmatter.related_posts.length > 0) {
- const explicitRelatedPosts = allPosts.filter(post =>
- frontmatter.related_posts.includes(post.slug)
- );
- relatedPosts = [...explicitRelatedPosts];
-}
-
-// If we need more related posts, find them by tags
-if (relatedPosts.length < 3 && frontmatter.tags && frontmatter.tags.length > 0) {
- // Calculate tag similarity score for each post
- const tagSimilarityPosts = allPosts
- .filter(post =>
- // Filter out current post and already included related posts
- post.data.title !== frontmatter.title &&
- !relatedPosts.some(rp => rp.slug === post.slug)
- )
- .map(post => {
- // Count matching tags
- const postTags = post.data.tags || [];
- const matchingTags = postTags.filter(tag =>
- frontmatter.tags.includes(tag)
- );
- return {
- post,
- score: matchingTags.length
- };
- })
- .filter(item => item.score > 0) // Only consider posts with at least one matching tag
- .sort((a, b) => b.score - a.score) // Sort by score descending
- .map(item => item.post); // Extract just the post
-
- // Add tag-related posts to fill up to 3 related posts
- relatedPosts = [...relatedPosts, ...tagSimilarityPosts.slice(0, 3 - relatedPosts.length)];
-}
-
-// Limit to 3 related posts
-relatedPosts = relatedPosts.slice(0, 3);
---
@@ -102,10 +101,10 @@ relatedPosts = relatedPosts.slice(0, 3);
{/* Tags */}
@@ -130,18 +130,6 @@ relatedPosts = relatedPosts.slice(0, 3);
)}
- {/* Content Connections Graph - only show if we have the current post and related content */}
- {currentPost && (frontmatter.tags?.length > 0 || relatedPosts.length > 0) && (
-
-
Content Connections
-
-
- )}
-
{/* Display Hero Image */}
{displayImage && (
@@ -149,106 +137,199 @@ relatedPosts = relatedPosts.slice(0, 3);
)}
+ {/* Content Connections - Knowledge Graph */}
+
+
Post Connections
+
+
+
{/* Main Content Area */}
{/* Renders the actual markdown content */}
+ {/* Related Posts Section */}
+ {relatedPosts.length > 0 && (
+
+ )}
{/* Sidebar */}
-
-{/* Script for Table of Contents Generation (Unchanged) */}
+
+
\ No newline at end of file