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

- -
- - +
+ +
+
+ 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 the filter buttons below to focus on specific topics
  • +
  • Use mouse wheel to zoom in/out and drag to pan
  • +
  • Click an empty area to reset the view
  • +
  • In fullscreen mode, click a post to view its full content
  • +
- -
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
-

Physics

- -
- - -
- -
- - -
- -
- - -
-
- -
- - -
+
- - {!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
  • -
-
-
-
- )} -
@@ -270,52 +145,86 @@ const defaultPhysics = { View Content
- - {!isMinimal && ( -
-
- - - + + + + +
+
+

Post Title

+ +
+ + + +
+
+ + + +
+
-
-
- )} - - - +
+
- -{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 }; +--- + + +
+

Post Connections

+
+
+ + + + + + \ 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'}

- - -
- -
-

How to use the Knowledge Graph

-
    -
  • Click on nodes to see connections and navigate
  • -
  • Drag nodes to rearrange the graph
  • -
  • Use the zoom controls to zoom in/out
  • -
  • Click the fullscreen button for a larger view
  • -
  • Adjust physics settings with the physics controls
  • -
-
-
- - -
- - -
-
- -
-
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- - -
- - - - -
- - - - - -
-
- - Current Article -
-
- - Related Articles -
-
- - Category -
-
- - Tags -
-
- - -
-
-
Loading graph...
-
- - -
-
-

Knowledge Graph Explorer

-
- -
-
-
-
-
-
- -

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);
- {/* Display Draft Badge First */} + {/* Display Draft Badge if needed */} {frontmatter.draft && DRAFT} - {/* Title (Smaller) */} + {/* Title */}

{frontmatter.title}

{/* Description */} @@ -118,6 +117,7 @@ relatedPosts = relatedPosts.slice(0, 3); (Updated {formattedUpdatedDate}) )} {frontmatter.readTime && {frontmatter.readTime}} + {frontmatter.category && }
{/* 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 */}
-