diff --git a/src/components/KnowledgeGraph.astro b/src/components/KnowledgeGraph.astro index c0e6eb1..5b947ab 100644 --- a/src/components/KnowledgeGraph.astro +++ b/src/components/KnowledgeGraph.astro @@ -192,7 +192,7 @@ const nodeTypeCounts = {
- + Read Full Article @@ -878,42 +878,7 @@ const nodeTypeCounts = { // Reset graph view if "All" is selected cy.elements().removeClass('highlighted faded filtered'); const allFilterBtn = document.querySelector('.graph-filter[data-filter="all"]'); -// Add event listener to prevent redirect in fullscreen mode - if (fullPostLink) { - fullPostLink.addEventListener('click', (e) => { - if (isFullscreen) { - // If in fullscreen, prevent default behavior to keep the user in the graph view - e.preventDefault(); - - // Instead, display a message to exit fullscreen to visit the full article - const message = document.createElement('div'); - message.className = 'fullscreen-message'; - message.textContent = 'Exit fullscreen to visit the full article page'; - message.style.position = 'absolute'; - message.style.bottom = '70px'; // Adjust as needed - message.style.left = '50%'; - message.style.transform = 'translateX(-50%)'; - message.style.background = 'rgba(0, 0, 0, 0.75)'; - message.style.color = 'white'; - message.style.padding = '8px 16px'; - message.style.borderRadius = '4px'; - message.style.zIndex = '1000'; - message.style.transition = 'opacity 0.3s ease'; - - fullPostContent.appendChild(message); - - // Remove the message after 3 seconds - setTimeout(() => { - message.style.opacity = '0'; - setTimeout(() => { - message.remove(); - }, 300); - }, 3000); - } - }); - } - - // Listen for ESC key to exit fullscreen +// Listen for ESC key to exit fullscreen document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isFullscreen) { toggleFullscreen(); @@ -958,41 +923,6 @@ const nodeTypeCounts = { // Dispatch graphReady event for external listeners document.dispatchEvent(new CustomEvent('graphReady', { detail: { cy } })); - // Add event listener to prevent redirect in fullscreen mode - if (fullPostLink) { - fullPostLink.addEventListener('click', (e) => { - if (isFullscreen) { - // If in fullscreen, prevent default behavior to keep the user in the graph view - e.preventDefault(); - - // Instead, display a message to exit fullscreen to visit the full article - const message = document.createElement('div'); - message.className = 'fullscreen-message'; - message.textContent = 'Exit fullscreen to visit the full article page'; - message.style.position = 'absolute'; - message.style.bottom = '70px'; // Adjust as needed - message.style.left = '50%'; - message.style.transform = 'translateX(-50%)'; - message.style.background = 'rgba(0, 0, 0, 0.75)'; - message.style.color = 'white'; - message.style.padding = '8px 16px'; - message.style.borderRadius = '4px'; - message.style.zIndex = '1000'; - message.style.transition = 'opacity 0.3s ease'; - - fullPostContent.appendChild(message); - - // Remove the message after 3 seconds - setTimeout(() => { - message.style.opacity = '0'; - setTimeout(() => { - message.remove(); - }, 300); - }, 3000); - } - }); - } - // Listen for ESC key to exit fullscreen document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && isFullscreen) { diff --git a/src/components/MiniGraph.astro b/src/components/MiniGraph.astro index 28eaa7a..65318f5 100644 --- a/src/components/MiniGraph.astro +++ b/src/components/MiniGraph.astro @@ -8,6 +8,8 @@ interface Props { title: string; // Current post title tags?: string[]; // Current post tags category?: string; // Current post category + relatedPosts?: any[]; // Related posts data + allPosts?: any[]; // All posts for second level relationships } // Extract props with defaults @@ -15,34 +17,127 @@ const { slug, title, tags = [], - category = "Uncategorized" + category = "Uncategorized", + relatedPosts = [], + allPosts = [] } = 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 +// 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 + +// 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)) { + return; + } + + // If post has the tag and isn't already added + if (post.data.tags?.includes(tag) && !level2PostIds.has(post.slug)) { + level2PostIds.add(post.slug); + level2Posts.push(post); + } + }); +}); + +// Get Level 2 tags from Level 2 posts +const level2Tags = new Set(); +level2Posts.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 = [ - // Current post node + // Level 0: Current post node { id: slug, label: title, - type: "post" + type: "post", + level: 0 }, - // Tag nodes + // Level 1: Tag nodes ...tags.map(tag => ({ id: `tag-${tag}`, label: tag, - type: "tag" + type: "tag", + level: 1 + })), + // Level 1: Related post nodes + ...relatedPosts.map(post => ({ + id: post.slug, + label: post.data.title, + type: "post", + level: 1 + })), + // Level 2: Related tags nodes + ...relatedPostsTags.map(tag => ({ + id: `tag-${tag}`, + label: tag, + type: "tag", + level: 2 + })), + // Level 2: Posts related to tags + ...level2Posts.map(post => ({ + id: post.slug, + label: post.data.title, + type: "post", + level: 2 + })), + // Level 2: Tags from Level 2 posts + ...[...level2Tags].map(tag => ({ + id: `tag-${tag}`, + label: tag.toString(), + type: "tag", + level: 2 })) ]; -// Create edges connecting post to tags -const edges = tags.map(tag => ({ - source: slug, - target: `tag-${tag}`, - type: "post-tag" -})); +// 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" + })) + ), + // 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" + })) + ) +]; // Prepare graph data object const graphData = { nodes, edges }; @@ -69,7 +164,7 @@ const graphData = { nodes, edges }; .mini-graph-container { width: 100%; - height: 200px; + aspect-ratio: 1 / 1; /* Create square aspect ratio */ border-radius: 8px; overflow: hidden; border: 1px solid var(--card-border, rgba(56, 189, 248, 0.2)); @@ -125,7 +220,8 @@ const graphData = { nodes, edges }; data: { id: node.id, label: node.label, - type: node.type + type: node.type, + level: node.level } })), ...graphData.edges.map((edge, index) => ({ @@ -155,47 +251,104 @@ const graphData = { nodes, edges }; 'text-max-width': '60px' } }, - // Post node style + // Current post node style (Level 0) { - selector: 'node[type="post"]', + selector: 'node[type="post"][level=0]', style: { 'background-color': '#06B6D4', 'width': 30, 'height': 30, 'font-size': '9px', - 'text-max-width': '80px' + 'text-max-width': '80px', + 'border-width': 2, + 'border-color': '#38BDF8' } }, - // Tag node style + // Related post node style (Level 1) { - selector: 'node[type="tag"]', + selector: 'node[type="post"][level=1]', + style: { + 'background-color': '#3B82F6', + 'width': 25, + 'height': 25, + 'font-size': '8px' + } + }, + // Distant post node style (Level 2) + { + selector: 'node[type="post"][level=2]', + style: { + 'background-color': '#60A5FA', + 'width': 20, + 'height': 20, + 'font-size': '7px', + 'opacity': 0.8 + } + }, + // Primary tag node style (Level 1) + { + selector: 'node[type="tag"][level=1]', style: { 'background-color': '#10B981', 'shape': 'diamond', - 'width': 18, - 'height': 18 + 'width': 20, + 'height': 20 } }, - // Edge style + // Secondary tag node style (Level 2) { - selector: 'edge', + selector: 'node[type="tag"][level=2]', style: { - 'width': 1, - 'line-color': 'rgba(16, 185, 129, 0.6)', + 'background-color': '#34D399', + 'shape': 'diamond', + 'width': 15, + 'height': 15, + 'opacity': 0.8 + } + }, + // Direct edge style + { + selector: 'edge[type="post-tag"]', + style: { + 'width': 1.5, + 'line-color': 'rgba(16, 185, 129, 0.7)', + 'line-style': 'solid', + 'curve-style': 'bezier', + 'opacity': 0.8 + } + }, + // Related post edge style + { + selector: 'edge[type="post-related"]', + style: { + 'width': 1.5, + 'line-color': 'rgba(59, 130, 246, 0.7)', 'line-style': 'dashed', 'curve-style': 'bezier', - 'opacity': 0.7 + 'opacity': 0.8 + } + }, + // Secondary connections + { + selector: 'edge[type="tag-post"]', + style: { + 'width': 1, + 'line-color': 'rgba(16, 185, 129, 0.4)', + 'line-style': 'dotted', + 'curve-style': 'bezier', + 'opacity': 0.6 } } ], - // Simple layout for small space + // Improved layout for multi-level visualization layout: { - name: 'concentric', + name: 'concentric', // NOTE: This is concentric, not cose as requested earlier concentric: function(node) { - return node.data('type') === 'post' ? 10 : 1; + // Use node level for concentric layout + return 10 - node.data('level') * 3; }, - levelWidth: function() { return 1; }, - minNodeSpacing: 50, + levelWidth: function() { return 2; }, + minNodeSpacing: 40, animate: false } }); @@ -207,6 +360,12 @@ const graphData = { nodes, edges }; window.location.href = `/tag/${tagName}`; }); + cy.on('tap', 'node[type="post"][level!=0]', function(evt) { + const node = evt.target; + const postSlug = node.id(); + window.location.href = `/posts/${postSlug}/`; + }); + // Fit graph to container cy.fit(undefined, 20); diff --git a/src/content/posts/Test.md b/src/content/posts/Test.md index a7bbae1..2e17100 100644 --- a/src/content/posts/Test.md +++ b/src/content/posts/Test.md @@ -1,14 +1,12 @@ --- -title: Secure Remote Access with Cloudflare Tunnels +title: This is a test description: How to set up Cloudflare Tunnels for secure remote access to your home lab services pubDate: Jul 22 2023 heroImage: /images/posts/prometheusk8.png category: networking tags: - - cloudflare - - networking - - security - - homelab - - tunnels + - Tag A + - Tag B + - Tag C readTime: "7 min read" --- diff --git a/src/layouts/BlogPostLayout.astro b/src/layouts/deleteBlogPostLayout.astro similarity index 91% rename from src/layouts/BlogPostLayout.astro rename to src/layouts/deleteBlogPostLayout.astro index 3082559..3f4d67e 100644 --- a/src/layouts/BlogPostLayout.astro +++ b/src/layouts/deleteBlogPostLayout.astro @@ -2,7 +2,8 @@ import BaseLayout from './BaseLayout.astro'; import Header from '../components/Header.astro'; import Footer from '../components/Footer.astro'; -import MiniKnowledgeGraph from '../components/MiniKnowledgeGraph.astro'; // Restore original or keep if needed +// import MiniKnowledgeGraph from '../components/MiniKnowledgeGraph.astro'; // Keep if needed elsewhere, but remove from main content +import MiniGraph from '../components/MiniGraph.astro'; // Add import for MiniGraph import { getCollection } from 'astro:content'; interface Props { @@ -137,11 +138,11 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
)} - {/* Content Connections - Knowledge Graph */} -
+ {/* Content Connections - Removed MiniKnowledgeGraph from here */} + {/*

Post Connections

-
+
*/} {/* Main Content Area */}
@@ -194,6 +195,24 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
+ {/* MiniGraph Component - Placed after Author Card */} + + + {/* Tags Section - Placed after MiniGraph */} +
+ {/* Table of Contents Card */}