--- // MiniGraph.astro - A standalone mini knowledge graph component with fullscreen capability // This component is designed to work independently from the blog structure and now includes content previews // 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 content?: string; // Current post content HTML relatedPosts?: any[]; // Related posts data with their content allPosts?: any[]; // All posts for second level relationships width?: string; // Optional width parameter } // Extract props with defaults const { slug, title, tags = [], category = "Uncategorized", content = "", // Add content property for current post relatedPosts = [], allPosts = [], width = "100%" // Default width of the component } = Astro.props; // Generate unique ID for the graph container const graphId = `graph-${Math.random().toString(36).substring(2, 8)}`; // Get all unique tags from related posts (Level 1 tags) const relatedPostsTags = relatedPosts .flatMap(post => post.data.tags || []) .filter(tag => !tags.includes(tag)); // Exclude current post tags to avoid duplicates // Create a set of all Level 1 nodes' tags for filtering Level 2 tags const level1TagsSet = new Set([...tags, ...relatedPostsTags]); // Get Level 2 posts: posts related to Level 1 tags (excluding current post and Level 1 posts) const level2PostIds = new Set(); const level2Posts = []; // For each Level 1 tag, find related posts tags.forEach(tag => { allPosts.forEach(post => { // Skip if post is current post or already in related posts if (post.slug === slug || relatedPosts.some(rp => rp.slug === post.slug) || level2PostIds.has(post.slug)) { return; } // If post has the tag, add it to Level 2 if (post.data.tags?.includes(tag)) { level2PostIds.add(post.slug); level2Posts.push(post); } }); }); // Only collect Level 2 tags that are directly linked to Level 1 posts // This fixes the issue where unrelated Level 2 tags were being included const level2Tags = new Set(); relatedPosts.forEach(post => { (post.data.tags || []).forEach(tag => { // Only add if not already in Level 0 or Level 1 tags if (!tags.includes(tag) && !relatedPostsTags.includes(tag)) { level2Tags.add(tag); } }); }); // Prepare nodes data with different levels const nodes = [ // Level 0: Current post node { id: slug, label: title, type: "post", level: 0, category: category, tags: tags, content: content, // Add content for the current post (as HTML string) url: `/posts/${slug}/` }, // Level 1: Tag nodes ...tags.map(tag => ({ id: `tag-${tag}`, label: tag, type: "tag", level: 1, url: `/tag/${tag}/` })), // Level 1: Related post nodes ...relatedPosts.map(post => { // Extract content from post object - this will vary based on your data structure // Try multiple properties where content might be stored let postContent = ''; if (post.data.content) { postContent = post.data.content; } else if (post.data.body) { postContent = post.data.body; } else if (post.data.html) { postContent = post.data.html; } else if (post.content) { postContent = post.content; } else if (post.body) { postContent = post.body; } else if (post.html) { postContent = post.html; } else if (post.data.excerpt) { postContent = post.data.excerpt; } return { id: post.slug, label: post.data.title, type: "post", level: 1, category: post.data.category || "Uncategorized", tags: post.data.tags || [], content: postContent, // Add content as HTML string url: `/posts/${post.slug}/` }; }), // Level 2: Related tags nodes (Tags from Level 1 posts) ...relatedPostsTags.map(tag => ({ id: `tag-${tag}`, label: tag, type: "tag", level: 2, url: `/tag/${tag}/` })), // Level 2: Posts related to tags (Posts connected to Level 1 tags) ...level2Posts.map(post => { // Extract content from level 2 posts let postContent = ''; if (post.data.content) { postContent = post.data.content; } else if (post.data.body) { postContent = post.data.body; } else if (post.data.html) { postContent = post.data.html; } else if (post.content) { postContent = post.content; } else if (post.body) { postContent = post.body; } else if (post.html) { postContent = post.html; } else if (post.data.excerpt) { postContent = post.data.excerpt; } return { id: post.slug, label: post.data.title, type: "post", level: 2, category: post.data.category || "Uncategorized", tags: post.data.tags || [], content: postContent, // Add content as HTML string url: `/posts/${post.slug}/` }; }), // Level 2: Tags from Level 1 posts (only tags directly connected to Level 1 posts) // This was the corrected logic for level2Tags Set ...[...level2Tags].map(tag => ({ id: `tag-${tag}`, label: tag.toString(), type: "tag", level: 2, url: `/tag/${tag.toString()}/` })) ]; // Create edges connecting nodes const edges = [ // Level 0 to Level 1: Current post to its tags ...tags.map(tag => ({ source: slug, target: `tag-${tag}`, type: "post-tag" })), // Level 0 to Level 1: Current post to related posts ...relatedPosts.map(post => ({ source: slug, target: post.slug, type: "post-related" })), // Level 1 to Level 2: Related posts to their tags ...relatedPosts.flatMap(post => (post.data.tags || []).map(tag => ({ source: post.slug, target: `tag-${tag}`, type: "post-tag" // Re-using post-tag type for simplicity })) ), // Level 1 to Level 2: Tags to related posts ...level2Posts.flatMap(post => tags.filter(tag => post.data.tags?.includes(tag)).map(tag => ({ source: `tag-${tag}`, target: post.slug, type: "tag-post" // New type for tag -> post connection })) ) ]; // Prepare graph data object const graphData = { nodes, edges }; // Define colors for categories const predefinedColors = { 'Kubernetes': '#326CE5', 'Docker': '#2496ED', 'DevOps': '#FF6F61', 'Homelab': '#06B6D4', 'Networking': '#9333EA', 'Infrastructure': '#10B981', 'Automation': '#F59E0B', 'Security': '#EF4444', 'Monitoring': '#6366F1', 'Storage': '#8B5CF6', 'Obsidian': '#7C3AED', 'Tutorial': '#3B82F6', 'Uncategorized': '#A0AEC0' }; ---
Select a post to view content preview