604 lines
18 KiB
Plaintext
604 lines
18 KiB
Plaintext
---
|
|
// The key change is moving the MiniKnowledgeGraph component BEFORE the tags section
|
|
// and styling it properly with clear z-index values to ensure proper display
|
|
|
|
import BaseLayout from '../layouts/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 { getCollection } from 'astro:content';
|
|
|
|
interface Props {
|
|
frontmatter: {
|
|
title: string;
|
|
description?: string;
|
|
pubDate: Date;
|
|
updatedDate?: Date;
|
|
heroImage?: string;
|
|
category?: string;
|
|
tags?: string[];
|
|
readTime?: string;
|
|
draft?: boolean;
|
|
author?: string;
|
|
github?: string;
|
|
live?: string;
|
|
technologies?: string[];
|
|
related_posts?: string[]; // Explicit related posts by slug
|
|
}
|
|
}
|
|
|
|
const { frontmatter } = Astro.props;
|
|
|
|
// Format dates
|
|
const formattedPubDate = frontmatter.pubDate ? new Date(frontmatter.pubDate).toLocaleDateString('en-us', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
}) : 'N/A';
|
|
|
|
const formattedUpdatedDate = frontmatter.updatedDate ? new Date(frontmatter.updatedDate).toLocaleDateString('en-us', {
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
}) : null;
|
|
|
|
// 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(error => {
|
|
console.error('Error fetching posts collection:', error);
|
|
return [];
|
|
});
|
|
|
|
// Try blog collection if posts collection doesn't exist
|
|
const blogPosts = allPosts.length === 0 ? await getCollection('blog').catch(() => []) : [];
|
|
const combinedPosts = [...allPosts, ...blogPosts];
|
|
|
|
// Find the current post in collection
|
|
const currentPost = combinedPosts.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 = combinedPosts.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 = combinedPosts
|
|
.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);
|
|
|
|
// Check if we can show the Knowledge Graph
|
|
const showKnowledgeGraph = currentPost || (frontmatter.tags?.length > 0 || relatedPosts.length > 0);
|
|
|
|
// Create fallback data if current post is missing
|
|
const fallbackCurrentPost = currentPost || {
|
|
slug: frontmatter.title.toLowerCase().replace(/\s+/g, '-'),
|
|
data: {
|
|
title: frontmatter.title,
|
|
tags: frontmatter.tags || [],
|
|
category: frontmatter.category || 'Uncategorized'
|
|
}
|
|
};
|
|
---
|
|
|
|
<BaseLayout title={frontmatter.title} description={frontmatter.description} image={displayImage}>
|
|
<Header slot="header" />
|
|
|
|
<div class="blog-post-container">
|
|
<article class="blog-post">
|
|
<header class="blog-post-header">
|
|
{/* Display Draft Badge First */}
|
|
{frontmatter.draft && <span class="draft-badge mb-4">DRAFT</span>}
|
|
|
|
{/* Title */}
|
|
<h1 class="blog-post-title mb-2">{frontmatter.title}</h1>
|
|
|
|
{/* Description */}
|
|
{frontmatter.description && <p class="blog-post-description mb-4">{frontmatter.description}</p>}
|
|
|
|
{/* Metadata (Date, Read Time) */}
|
|
<div class="blog-post-meta mb-4">
|
|
<span class="blog-post-date">Published {formattedPubDate}</span>
|
|
{formattedUpdatedDate && (
|
|
<span class="blog-post-updated">(Updated {formattedUpdatedDate})</span>
|
|
)}
|
|
{frontmatter.readTime && <span class="blog-post-read-time">{frontmatter.readTime}</span>}
|
|
</div>
|
|
|
|
{/* Debug information */}
|
|
<div class="debug-info" style="background: rgba(255,0,0,0.1); padding: 10px; margin-bottom: 20px; border-radius: 5px; display: block;">
|
|
<p><strong>Debug Info:</strong></p>
|
|
<p>ShowKnowledgeGraph: {showKnowledgeGraph ? 'true' : 'false'}</p>
|
|
<p>CurrentPost exists: {currentPost ? 'yes' : 'no'}</p>
|
|
<p>Has Tags: {frontmatter.tags?.length > 0 ? 'yes' : 'no'}</p>
|
|
<p>Has Related Posts: {relatedPosts.length > 0 ? 'yes' : 'no'}</p>
|
|
<p>Current Post Tags: {JSON.stringify(frontmatter.tags || [])}</p>
|
|
</div>
|
|
|
|
{/* IMPORTANT CHANGE: Knowledge Graph - Display BEFORE tags */}
|
|
{showKnowledgeGraph && (
|
|
<div class="mini-knowledge-graph-wrapper">
|
|
<MiniKnowledgeGraph
|
|
currentPost={fallbackCurrentPost}
|
|
relatedPosts={relatedPosts}
|
|
height="250px"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Tags - Now placed AFTER the knowledge graph */}
|
|
{frontmatter.tags && frontmatter.tags.length > 0 && (
|
|
<div class="blog-post-tags">
|
|
{frontmatter.tags.map((tag) => (
|
|
<a href={`/tag/${tag}`} class="blog-post-tag">#{tag}</a>
|
|
))}
|
|
</div>
|
|
)}
|
|
</header>
|
|
|
|
{/* Display Hero Image */}
|
|
{displayImage && (
|
|
<div class="blog-post-hero">
|
|
<img src={displayImage.startsWith('/') ? displayImage : `/${displayImage}`} alt={frontmatter.title} width="1024" height="512" loading="lazy" />
|
|
</div>
|
|
)}
|
|
|
|
{/* Main Content Area */}
|
|
<div class="blog-post-content prose prose-invert max-w-none">
|
|
<slot /> {/* Renders the actual markdown content */}
|
|
</div>
|
|
|
|
</article>
|
|
|
|
{/* Sidebar */}
|
|
<aside class="blog-post-sidebar">
|
|
{/* Author Card Updated */}
|
|
<div class="sidebar-card author-card">
|
|
<div class="author-avatar">
|
|
<img src="/images/avatar.jpg" alt="LaForceIT Tech Blogs" />
|
|
</div>
|
|
<div class="author-info">
|
|
<h3>LaForceIT.com Tech Blogs</h3>
|
|
<p>For Home Labbers, Technologists & Engineers</p>
|
|
</div>
|
|
<p class="author-bio">
|
|
Exploring enterprise-grade infrastructure, automation, Kubernetes, and zero-trust networking in the home lab and beyond.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Table of Contents Card */}
|
|
<div class="sidebar-card toc-card">
|
|
<h3>Table of Contents</h3>
|
|
<nav class="toc-container" id="toc">
|
|
<p class="text-sm text-gray-400">Loading TOC...</p>
|
|
</nav>
|
|
</div>
|
|
|
|
{/* Related Posts */}
|
|
{relatedPosts.length > 0 && (
|
|
<div class="sidebar-card related-posts-card">
|
|
<h3>Related Articles</h3>
|
|
<div class="related-posts">
|
|
{relatedPosts.map(post => (
|
|
<a href={`/posts/${post.slug}/`} class="related-post-link">
|
|
<h4>{post.data.title}</h4>
|
|
{post.data.tags && post.data.tags.length > 0 && (
|
|
<div class="related-post-tags">
|
|
{post.data.tags.slice(0, 2).map(tag => (
|
|
<span class="related-tag">{tag}</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</aside>
|
|
</div>
|
|
|
|
<Newsletter />
|
|
<Footer slot="footer" />
|
|
</BaseLayout>
|
|
|
|
{/* Script for Table of Contents Generation */}
|
|
<script>
|
|
function generateToc() {
|
|
const tocContainer = document.getElementById('toc');
|
|
const contentArea = document.querySelector('.blog-post-content');
|
|
if (!tocContainer || !contentArea) return;
|
|
const headings = contentArea.querySelectorAll('h2, h3');
|
|
if (headings.length > 0) {
|
|
const tocList = document.createElement('ul');
|
|
tocList.className = 'toc-list';
|
|
headings.forEach((heading) => {
|
|
let id = heading.id;
|
|
if (!id) {
|
|
id = heading.textContent?.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').replace(/--+/g, '-') || `heading-${Math.random().toString(36).substring(7)}`;
|
|
heading.id = id;
|
|
}
|
|
const listItem = document.createElement('li');
|
|
listItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`;
|
|
const link = document.createElement('a');
|
|
link.href = `#${id}`;
|
|
link.textContent = heading.textContent;
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
|
|
});
|
|
listItem.appendChild(link);
|
|
tocList.appendChild(listItem);
|
|
});
|
|
tocContainer.innerHTML = '';
|
|
tocContainer.appendChild(tocList);
|
|
} else {
|
|
tocContainer.innerHTML = '<p class="text-sm text-gray-400">No sections found.</p>';
|
|
}
|
|
}
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', generateToc);
|
|
} else {
|
|
generateToc();
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.draft-badge {
|
|
display: inline-block;
|
|
margin-bottom: 1rem;
|
|
padding: 0.25rem 0.75rem;
|
|
background-color: rgba(234, 179, 8, 0.2);
|
|
color: #ca8a04;
|
|
font-size: 0.8rem;
|
|
border-radius: 0.25rem;
|
|
font-weight: 600;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
}
|
|
.blog-post-container {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) 300px;
|
|
gap: 2rem;
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
padding: 2rem 1rem;
|
|
}
|
|
.blog-post-header {
|
|
margin-bottom: 2.5rem;
|
|
border-bottom: 1px solid var(--card-border);
|
|
padding-bottom: 1.5rem;
|
|
}
|
|
.blog-post-title {
|
|
font-size: clamp(1.8rem, 4vw, 2.5rem);
|
|
line-height: 1.25;
|
|
margin-bottom: 0.75rem;
|
|
color: var(--text-primary);
|
|
}
|
|
.blog-post-description {
|
|
font-size: 1.1rem;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 1.5rem;
|
|
max-width: 75ch;
|
|
}
|
|
.blog-post-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
font-size: 0.85rem;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Knowledge Graph Container - Improved styles for proper placement */
|
|
.mini-knowledge-graph-wrapper {
|
|
width: 100%;
|
|
margin: 0 0 1.5rem;
|
|
position: relative;
|
|
z-index: 2; /* Ensure proper stacking context */
|
|
display: block !important; /* Force display */
|
|
min-height: 250px; /* Ensure minimum height */
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
/* Add additional styles to improve visibility */
|
|
background: var(--card-bg, #1e293b);
|
|
border: 1px solid var(--card-border, #334155);
|
|
height: 250px; /* Explicit height */
|
|
visibility: visible !important; /* Force visibility */
|
|
}
|
|
|
|
.blog-post-tags {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.75rem;
|
|
margin-top: 1.5rem; /* Increased margin-top to separate from graph */
|
|
position: relative;
|
|
z-index: 2;
|
|
}
|
|
.blog-post-tag {
|
|
color: var(--accent-secondary);
|
|
text-decoration: none;
|
|
font-size: 0.85rem;
|
|
transition: color 0.3s ease;
|
|
font-family: 'JetBrains Mono', monospace;
|
|
background-color: rgba(59, 130, 246, 0.1);
|
|
padding: 0.2rem 0.6rem;
|
|
border-radius: 4px;
|
|
}
|
|
.blog-post-tag:hover {
|
|
color: var(--accent-primary);
|
|
background-color: rgba(6, 182, 212, 0.15);
|
|
}
|
|
.blog-post-hero {
|
|
width: 100%;
|
|
margin-bottom: 2.5rem;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
border: 1px solid var(--card-border);
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
.blog-post-hero img {
|
|
width: 100%;
|
|
height: auto;
|
|
display: block;
|
|
}
|
|
|
|
.blog-post-sidebar {
|
|
position: sticky;
|
|
top: 2rem;
|
|
align-self: start;
|
|
height: calc(100vh - 4rem);
|
|
overflow-y: auto;
|
|
}
|
|
.sidebar-card {
|
|
background: var(--card-bg);
|
|
border: 1px solid var(--card-border);
|
|
border-radius: 10px;
|
|
padding: 1.5rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.author-card {
|
|
text-align: center;
|
|
}
|
|
.author-avatar {
|
|
width: 80px;
|
|
height: 80px;
|
|
border-radius: 50%;
|
|
overflow: hidden;
|
|
margin: 0 auto 1rem;
|
|
border: 2px solid var(--accent-primary);
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
.author-avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
.author-info h3 {
|
|
margin-bottom: 0.25rem;
|
|
color: var(--text-primary);
|
|
font-size: 1.1rem;
|
|
}
|
|
.author-info p {
|
|
color: var(--text-secondary);
|
|
margin-bottom: 1rem;
|
|
font-size: 0.9rem;
|
|
}
|
|
.author-bio {
|
|
font-size: 0.9rem;
|
|
margin-bottom: 0;
|
|
color: var(--text-secondary);
|
|
text-align: left;
|
|
}
|
|
|
|
.toc-card h3 {
|
|
margin-bottom: 1rem;
|
|
color: var(--text-primary);
|
|
}
|
|
.toc-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
max-height: 60vh;
|
|
overflow-y: auto;
|
|
}
|
|
.toc-item {
|
|
margin-bottom: 0.9rem;
|
|
}
|
|
.toc-item a {
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
transition: color 0.3s ease;
|
|
font-size: 0.9rem;
|
|
display: block;
|
|
padding-left: 0;
|
|
line-height: 1.4;
|
|
}
|
|
.toc-item a:hover {
|
|
color: var(--accent-primary);
|
|
}
|
|
.toc-h3 a {
|
|
padding-left: 1.5rem;
|
|
font-size: 0.85rem;
|
|
opacity: 0.9;
|
|
}
|
|
|
|
/* Related Posts */
|
|
.related-posts-card h3 {
|
|
margin-bottom: 1rem;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.related-posts {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.related-post-link {
|
|
display: block;
|
|
padding: 0.75rem;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--border-primary);
|
|
background: rgba(255, 255, 255, 0.03);
|
|
transition: all 0.3s ease;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.related-post-link:hover {
|
|
background: rgba(6, 182, 212, 0.05);
|
|
border-color: var(--accent-primary);
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.related-post-link h4 {
|
|
margin: 0 0 0.5rem;
|
|
font-size: 0.95rem;
|
|
color: var(--text-primary);
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.related-post-tags {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.related-tag {
|
|
font-size: 0.7rem;
|
|
padding: 0.1rem 0.4rem;
|
|
border-radius: 3px;
|
|
background: rgba(16, 185, 129, 0.1);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.blog-post-container {
|
|
grid-template-columns: 1fr; /* Stack on smaller screens */
|
|
}
|
|
.blog-post-sidebar {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<!-- Table of Contents Generator -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const headings = document.querySelectorAll('.post-content h2, .post-content h3');
|
|
if (headings.length === 0) return;
|
|
|
|
const tocContainer = document.getElementById('table-of-contents');
|
|
if (!tocContainer) return;
|
|
|
|
const toc = document.createElement('ul');
|
|
|
|
headings.forEach((heading, index) => {
|
|
// Add ID to heading if it doesn't have one
|
|
if (!heading.id) {
|
|
heading.id = `heading-${index}`;
|
|
}
|
|
|
|
const li = document.createElement('li');
|
|
const a = document.createElement('a');
|
|
a.href = `#${heading.id}`;
|
|
a.textContent = heading.textContent;
|
|
|
|
// Add appropriate class based on heading level
|
|
if (heading.tagName === 'H3') {
|
|
li.style.paddingLeft = '1rem';
|
|
}
|
|
|
|
li.appendChild(a);
|
|
toc.appendChild(li);
|
|
});
|
|
|
|
tocContainer.appendChild(toc);
|
|
});
|
|
</script>
|
|
|
|
{/* Script to ensure the knowledge graph initializes properly */}
|
|
<script>
|
|
// Knowledge Graph Initialization Helper
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Generate TOC as before
|
|
generateToc();
|
|
|
|
// Ensure the knowledge graph initializes properly
|
|
initializeKnowledgeGraph();
|
|
});
|
|
|
|
function initializeKnowledgeGraph() {
|
|
const graphContainer = document.querySelector('.mini-knowledge-graph-wrapper');
|
|
|
|
if (graphContainer && !graphContainer.dataset.initialized) {
|
|
// Mark as initialized to prevent multiple initializations
|
|
graphContainer.dataset.initialized = 'true';
|
|
|
|
// Ensure container is visible
|
|
graphContainer.style.display = 'block';
|
|
graphContainer.style.visibility = 'visible';
|
|
graphContainer.style.height = '250px';
|
|
|
|
// Force a reflow/repaint
|
|
void graphContainer.offsetHeight;
|
|
|
|
// Add debugging log
|
|
console.log('Knowledge graph container found with dimensions:',
|
|
graphContainer.offsetWidth, 'x', graphContainer.offsetHeight);
|
|
|
|
// Check if the container is visible in viewport
|
|
const rect = graphContainer.getBoundingClientRect();
|
|
const isVisible = rect.top >= 0 &&
|
|
rect.left >= 0 &&
|
|
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
|
|
|
console.log('Knowledge graph is in viewport:', isVisible);
|
|
|
|
// If we have a script to initialize Cytoscape from MiniKnowledgeGraph.astro,
|
|
// it should run automatically. This just ensures the container is ready.
|
|
}
|
|
}
|
|
|
|
// Also try after window load, when all resources are fully loaded
|
|
window.addEventListener('load', () => {
|
|
setTimeout(initializeKnowledgeGraph, 500);
|
|
});
|
|
</script>
|