fresh-main #7

Merged
argonaut merged 4 commits from fresh-main into main 2025-04-26 20:46:13 +00:00
4 changed files with 2356 additions and 3250 deletions
Showing only changes of commit 336238e758 - Show all commits

File diff suppressed because it is too large Load Diff

View File

@ -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 };
---
<!-- Super simple HTML structure -->
<div class="knowledge-graph-wrapper">
<h4 class="graph-title">Post Connections</h4>
<div id={graphId} class="mini-graph-container"></div>
</div>
<!-- Minimal CSS -->
<style>
.knowledge-graph-wrapper {
width: 100%;
margin-bottom: 1rem;
}
.graph-title {
font-size: 1rem;
margin-bottom: 0.5rem;
color: var(--text-primary, #e2e8f0);
}
.mini-graph-container {
width: 100%;
height: 200px;
border-radius: 8px;
overflow: hidden;
border: 1px solid var(--card-border, rgba(56, 189, 248, 0.2));
background: rgba(15, 23, 42, 0.2);
}
</style>
<!-- Standalone initialization script -->
<script define:vars={{ graphId, graphData }}>
// Wait for page to fully load
window.addEventListener('load', function() {
// Retry initialization multiple times in case Cytoscape or the DOM isn't ready yet
let retries = 0;
const maxRetries = 5;
const retryInterval = 500; // ms
function initGraph() {
// Ensure Cytoscape is loaded
if (typeof cytoscape === 'undefined') {
console.warn(`[MiniGraph] Cytoscape not loaded, retry ${retries+1}/${maxRetries}...`);
if (retries < maxRetries) {
retries++;
setTimeout(initGraph, retryInterval);
} else {
console.error("[MiniGraph] Cytoscape library not available after multiple attempts.");
const container = document.getElementById(graphId);
if (container) {
container.innerHTML = '<div style="padding:10px;color:#a0aec0;text-align:center;">Graph library not loaded</div>';
}
}
return;
}
// Verify container exists
const container = document.getElementById(graphId);
if (!container) {
console.error(`[MiniGraph] Container #${graphId} not found.`);
return;
}
try {
// Check if we have any nodes
if (graphData.nodes.length === 0) {
container.innerHTML = '<div style="padding:10px;color:#a0aec0;text-align:center;">No connections</div>';
return;
}
// Initialize Cytoscape
const cy = cytoscape({
container,
elements: [
...graphData.nodes.map(node => ({
data: {
id: node.id,
label: node.label,
type: node.type
}
})),
...graphData.edges.map((edge, index) => ({
data: {
id: `e${index}`,
source: edge.source,
target: edge.target,
type: edge.type
}
}))
],
style: [
// Base node style
{
selector: 'node',
style: {
'background-color': '#3B82F6',
'label': 'data(label)',
'width': 20,
'height': 20,
'font-size': '8px',
'color': '#E2E8F0',
'text-valign': 'bottom',
'text-halign': 'center',
'text-margin-y': 5,
'text-wrap': 'ellipsis',
'text-max-width': '60px'
}
},
// Post node style
{
selector: 'node[type="post"]',
style: {
'background-color': '#06B6D4',
'width': 30,
'height': 30,
'font-size': '9px',
'text-max-width': '80px'
}
},
// Tag node style
{
selector: 'node[type="tag"]',
style: {
'background-color': '#10B981',
'shape': 'diamond',
'width': 18,
'height': 18
}
},
// Edge style
{
selector: 'edge',
style: {
'width': 1,
'line-color': 'rgba(16, 185, 129, 0.6)',
'line-style': 'dashed',
'curve-style': 'bezier',
'opacity': 0.7
}
}
],
// Simple layout for small space
layout: {
name: 'concentric',
concentric: function(node) {
return node.data('type') === 'post' ? 10 : 1;
},
levelWidth: function() { return 1; },
minNodeSpacing: 50,
animate: false
}
});
// Make nodes clickable
cy.on('tap', 'node[type="tag"]', function(evt) {
const node = evt.target;
const tagName = node.data('label');
window.location.href = `/tag/${tagName}`;
});
// Fit graph to container
cy.fit(undefined, 20);
} catch (error) {
console.error('[MiniGraph] Error initializing graph:', error);
container.innerHTML = '<div style="padding:10px;color:#a0aec0;text-align:center;">Error loading graph</div>';
}
}
// Start initialization attempt
initGraph();
});
</script>

File diff suppressed because it is too large Load Diff

View File

@ -2,8 +2,7 @@
import BaseLayout from './BaseLayout.astro'; import BaseLayout from './BaseLayout.astro';
import Header from '../components/Header.astro'; import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
import Newsletter from '../components/Newsletter.astro'; import MiniKnowledgeGraph from '../components/MiniKnowledgeGraph.astro'; // Restore original or keep if needed
import MiniKnowledgeGraph from '../components/MiniKnowledgeGraph.astro';
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
interface Props { interface Props {
@ -18,16 +17,68 @@ interface Props {
readTime?: string; readTime?: string;
draft?: boolean; draft?: boolean;
author?: string; author?: string;
github?: string; // Field for explicitly related posts
live?: string; related_posts?: string[];
technologies?: string[]; },
related_posts?: string[]; // Explicit related posts by slug 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', { const formattedPubDate = frontmatter.pubDate ? new Date(frontmatter.pubDate).toLocaleDateString('en-us', {
year: 'numeric', year: 'numeric',
month: 'long', month: 'long',
@ -42,58 +93,6 @@ const formattedUpdatedDate = frontmatter.updatedDate ? new Date(frontmatter.upda
// Default image if heroImage is missing // Default image if heroImage is missing
const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'; 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);
--- ---
<BaseLayout title={frontmatter.title} description={frontmatter.description} image={displayImage}> <BaseLayout title={frontmatter.title} description={frontmatter.description} image={displayImage}>
@ -102,10 +101,10 @@ relatedPosts = relatedPosts.slice(0, 3);
<div class="blog-post-container"> <div class="blog-post-container">
<article class="blog-post"> <article class="blog-post">
<header class="blog-post-header"> <header class="blog-post-header">
{/* Display Draft Badge First */} {/* Display Draft Badge if needed */}
{frontmatter.draft && <span class="draft-badge mb-4">DRAFT</span>} {frontmatter.draft && <span class="draft-badge mb-4">DRAFT</span>}
{/* Title (Smaller) */} {/* Title */}
<h1 class="blog-post-title mb-2">{frontmatter.title}</h1> <h1 class="blog-post-title mb-2">{frontmatter.title}</h1>
{/* Description */} {/* Description */}
@ -118,6 +117,7 @@ relatedPosts = relatedPosts.slice(0, 3);
<span class="blog-post-updated">(Updated {formattedUpdatedDate})</span> <span class="blog-post-updated">(Updated {formattedUpdatedDate})</span>
)} )}
{frontmatter.readTime && <span class="blog-post-read-time">{frontmatter.readTime}</span>} {frontmatter.readTime && <span class="blog-post-read-time">{frontmatter.readTime}</span>}
{frontmatter.category && <span class="blog-post-category">{frontmatter.category}</span>}
</div> </div>
{/* Tags */} {/* Tags */}
@ -130,18 +130,6 @@ relatedPosts = relatedPosts.slice(0, 3);
)} )}
</header> </header>
{/* Content Connections Graph - only show if we have the current post and related content */}
{currentPost && (frontmatter.tags?.length > 0 || relatedPosts.length > 0) && (
<div class="content-connections">
<h3 class="section-subtitle">Content Connections</h3>
<MiniKnowledgeGraph
currentPost={currentPost}
relatedPosts={relatedPosts}
height="250px"
/>
</div>
)}
{/* Display Hero Image */} {/* Display Hero Image */}
{displayImage && ( {displayImage && (
<div class="blog-post-hero"> <div class="blog-post-hero">
@ -149,106 +137,199 @@ relatedPosts = relatedPosts.slice(0, 3);
</div> </div>
)} )}
{/* Content Connections - Knowledge Graph */}
<div class="content-connections">
<h3 class="connections-title">Post Connections</h3>
<MiniKnowledgeGraph currentPost={currentPost} relatedPosts={relatedPosts} />
</div>
{/* Main Content Area */} {/* Main Content Area */}
<div class="blog-post-content prose prose-invert max-w-none"> <div class="blog-post-content prose prose-invert max-w-none">
<slot /> {/* Renders the actual markdown content */} <slot /> {/* Renders the actual markdown content */}
</div> </div>
{/* Related Posts Section */}
{relatedPosts.length > 0 && (
<div class="related-posts-section">
<h3 class="related-title">Related Content</h3>
<div class="related-posts-grid">
{relatedPosts.map((post) => (
<a href={`/posts/${post.slug}/`} class="related-post-card">
<div class="related-post-content">
<h4>{post.data.title}</h4>
<p>{post.data.description ?
(post.data.description.length > 100 ?
post.data.description.substring(0, 100) + '...' :
post.data.description) :
'Read more about this related topic.'}</p>
</div>
</a>
))}
</div>
</div>
)}
</article> </article>
{/* Sidebar */} {/* Sidebar */}
<aside class="blog-post-sidebar"> <aside class="blog-post-sidebar">
{/* Author Card Updated */} {/* Author Card */}
<div class="sidebar-card author-card"> <div class="sidebar-card author-card">
<div class="author-avatar"> <div class="author-avatar">
<img src="/images/avatar.jpg" alt="LaForceIT Tech Blogs" /> <div class="avatar-placeholder">DL</div>
</div> </div>
<div class="author-info"> <div class="author-info">
<h3>LaForceIT.com Tech Blogs</h3> <h3>Daniel LaForce</h3>
<p>For Home Labbers, Technologists & Engineers</p> <p>Infrastructure & DevOps Engineer</p>
</div> </div>
<p class="author-bio"> <p class="author-bio">
Exploring enterprise-grade infrastructure, automation, Kubernetes, and zero-trust networking in the home lab and beyond. Exploring enterprise-grade infrastructure, automation, Kubernetes, and self-hosted solutions for the modern home lab.
</p> </p>
<div class="author-links">
<a href="https://github.com/keyargo" target="_blank" rel="noopener noreferrer" class="author-link github">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"></path>
</svg>
GitHub
</a>
</div>
</div> </div>
{/* Table of Contents Card */} {/* Table of Contents Card */}
<div class="sidebar-card toc-card"> <div class="sidebar-card toc-card">
<h3>Table of Contents</h3> <h3>Table of Contents</h3>
<nav class="toc-container" id="toc"> <nav class="toc-container" id="toc">
<p class="text-sm text-gray-400">Loading TOC...</p> <p class="text-sm text-gray-400">Loading Table of Contents...</p>
</nav> </nav>
</div> </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> </aside>
</div> </div>
<Newsletter />
<Footer slot="footer" /> <Footer slot="footer" />
</BaseLayout> </BaseLayout>
{/* Script for Table of Contents Generation (Unchanged) */}
<script> <script>
function generateToc() { // Table of Contents Generator
document.addEventListener('DOMContentLoaded', () => {
const tocContainer = document.getElementById('toc'); const tocContainer = document.getElementById('toc');
const contentArea = document.querySelector('.blog-post-content'); const contentArea = document.querySelector('.blog-post-content');
if (!tocContainer || !contentArea) return; if (!tocContainer || !contentArea) return;
// Get all headings (h2, h3) from the content
const headings = contentArea.querySelectorAll('h2, h3'); const headings = contentArea.querySelectorAll('h2, h3');
if (headings.length > 0) {
if (headings.length === 0) {
tocContainer.innerHTML = '<p class="toc-empty">No sections found in this article.</p>';
return;
}
// Create the TOC list
const tocList = document.createElement('ul'); const tocList = document.createElement('ul');
tocList.className = 'toc-list'; tocList.className = 'toc-list';
headings.forEach((heading) => {
let id = heading.id; headings.forEach((heading, index) => {
if (!id) { // Add ID to heading if it doesn't have one
id = heading.textContent?.toLowerCase().replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').replace(/--+/g, '-') || `heading-${Math.random().toString(36).substring(7)}`; if (!heading.id) {
heading.id = id; heading.id = `heading-${index}`;
} }
// Create list item
const listItem = document.createElement('li'); const listItem = document.createElement('li');
listItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`; listItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`;
// Create link
const link = document.createElement('a'); const link = document.createElement('a');
link.href = `#${id}`; link.href = `#${heading.id}`;
link.textContent = heading.textContent; link.textContent = heading.textContent;
link.addEventListener('click', (e) => { link.addEventListener('click', (e) => {
e.preventDefault(); e.preventDefault();
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' }); document.getElementById(heading.id)?.scrollIntoView({
behavior: 'smooth',
block: 'start'
}); });
});
// Add to list
listItem.appendChild(link); listItem.appendChild(link);
tocList.appendChild(listItem); tocList.appendChild(listItem);
}); });
// Replace loading message with the TOC
tocContainer.innerHTML = ''; tocContainer.innerHTML = '';
tocContainer.appendChild(tocList); tocContainer.appendChild(tocList);
} else {
tocContainer.innerHTML = '<p class="text-sm text-gray-400">No sections found.</p>'; // Add smooth scrolling for all links pointing to headings
} document.querySelectorAll('a[href^="#heading-"]').forEach(anchor => {
} anchor.addEventListener('click', function(e) {
if (document.readyState === 'loading') { e.preventDefault();
document.addEventListener('DOMContentLoaded', generateToc); const targetId = this.getAttribute('href');
} else { document.querySelector(targetId)?.scrollIntoView({
generateToc(); behavior: 'smooth',
} block: 'start'
});
});
});
});
</script> </script>
<style is:global>
/* Table of Contents Styles */
.toc-list {
list-style: none;
padding: 0;
margin: 0;
}
.toc-item {
margin-bottom: 0.75rem;
}
.toc-item a {
color: var(--text-secondary);
text-decoration: none;
transition: color 0.2s ease;
font-size: 0.9rem;
display: block;
}
.toc-item a:hover {
color: var(--accent-primary);
}
.toc-h3 {
padding-left: 1rem;
font-size: 0.85rem;
}
.toc-empty {
color: var(--text-tertiary);
font-style: italic;
font-size: 0.9rem;
}
</style>
<style> <style>
.blog-post-container {
display: grid;
grid-template-columns: 7fr 3fr;
gap: 2rem;
max-width: 1200px;
margin: 2rem auto;
padding: 0 1.5rem;
}
.blog-post {
background: var(--card-bg);
border-radius: 12px;
border: 1px solid var(--card-border);
overflow: hidden;
padding: 2rem;
}
.blog-post-header {
margin-bottom: 2rem;
}
.draft-badge { .draft-badge {
display: inline-block; display: inline-block;
margin-bottom: 1rem; margin-bottom: 1rem;
@ -258,116 +339,162 @@ relatedPosts = relatedPosts.slice(0, 3);
font-size: 0.8rem; font-size: 0.8rem;
border-radius: 0.25rem; border-radius: 0.25rem;
font-weight: 600; font-weight: 600;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
}
.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 { .blog-post-title {
/* Made title slightly smaller */
font-size: clamp(1.8rem, 4vw, 2.5rem); font-size: clamp(1.8rem, 4vw, 2.5rem);
line-height: 1.25; /* Adjusted line height */ line-height: 1.2;
margin-bottom: 0.75rem; /* Adjusted margin */ margin-bottom: 0.75rem;
color: var(--text-primary); color: var(--text-primary);
} }
.blog-post-description { .blog-post-description {
font-size: 1.1rem; font-size: 1.1rem;
color: var(--text-secondary); color: var(--text-secondary);
margin-bottom: 1.5rem; /* Increased margin */ margin-bottom: 1.5rem;
max-width: 75ch; /* Adjusted width */ max-width: 75ch;
} }
.blog-post-meta { .blog-post-meta {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5rem 1.5rem; gap: 0.5rem 1.5rem;
margin-bottom: 1.5rem; /* Increased margin */ margin-bottom: 1.5rem;
font-size: 0.85rem; font-size: 0.85rem;
color: var(--text-secondary); color: var(--text-secondary);
} }
.blog-post-category {
padding: 0.25rem 0.75rem;
background: rgba(6, 182, 212, 0.1);
border-radius: 2rem;
font-family: var(--font-mono);
}
.blog-post-tags { .blog-post-tags {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.75rem; gap: 0.75rem;
margin-top: 0rem; /* Removed top margin */ margin-bottom: 1.5rem;
} }
.blog-post-tag { .blog-post-tag {
color: var(--accent-secondary); color: var(--accent-secondary);
text-decoration: none; text-decoration: none;
font-size: 0.85rem; font-size: 0.85rem;
transition: color 0.3s ease; transition: color 0.3s ease;
font-family: 'JetBrains Mono', monospace; font-family: var(--font-mono);
background-color: rgba(59, 130, 246, 0.1); background-color: rgba(59, 130, 246, 0.1);
padding: 0.2rem 0.6rem; padding: 0.2rem 0.6rem;
border-radius: 4px; border-radius: 4px;
} }
.blog-post-tag:hover { .blog-post-tag:hover {
color: var(--accent-primary); color: var(--accent-primary);
background-color: rgba(6, 182, 212, 0.15); background-color: rgba(6, 182, 212, 0.15);
transform: translateY(-2px);
} }
.blog-post-hero { .blog-post-hero {
width: 100%; width: 100%;
margin-bottom: 2.5rem; margin-bottom: 2rem;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
border: 1px solid var(--card-border); border: 1px solid var(--card-border);
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
} }
.blog-post-hero img { .blog-post-hero img {
width: 100%; width: 100%;
height: auto; height: auto;
display: block; display: block;
} }
/* Content Connections Graph */ /* Content Connections - Knowledge Graph */
.content-connections { .content-connections {
margin: 1.5rem 0 2rem; margin: 2rem 0;
border-radius: 10px; padding: 1.5rem;
border: 1px solid var(--card-border, #334155); background: var(--bg-secondary);
background: rgba(15, 23, 42, 0.2); border-radius: 8px;
overflow: hidden; border: 1px solid var(--card-border);
} }
.section-subtitle { .connections-title {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 600; margin-bottom: 1rem;
color: var(--text-primary, #e2e8f0); color: var(--text-primary);
padding: 1rem 1.5rem;
margin: 0;
background: rgba(15, 23, 42, 0.5);
border-bottom: 1px solid var(--card-border, #334155);
} }
.blog-post-content { /* Related Posts Section */
/* Styles inherited from prose */ .related-posts-section {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--card-border);
} }
.related-title {
font-size: 1.5rem;
margin-bottom: 1.5rem;
color: var(--text-primary);
}
.related-posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
.related-post-card {
background: var(--bg-secondary);
border-radius: 8px;
border: 1px solid var(--card-border);
padding: 1.5rem;
text-decoration: none;
transition: all 0.3s ease;
}
.related-post-card:hover {
transform: translateY(-3px);
border-color: var(--accent-primary);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.related-post-content h4 {
color: var(--text-primary);
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
.related-post-content p {
color: var(--text-secondary);
font-size: 0.9rem;
}
/* Sidebar */
.blog-post-sidebar { .blog-post-sidebar {
position: sticky; position: sticky;
top: 2rem; top: 2rem;
align-self: start; align-self: start;
height: calc(100vh - 4rem); height: calc(100vh - 4rem);
overflow-y: auto; overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2rem;
} }
.sidebar-card { .sidebar-card {
background: var(--card-bg); background: var(--card-bg);
border-radius: 12px;
border: 1px solid var(--card-border); border: 1px solid var(--card-border);
border-radius: 10px;
padding: 1.5rem; padding: 1.5rem;
margin-bottom: 1.5rem;
} }
/* Author Card */
.author-card { .author-card {
text-align: center; text-align: center;
} }
.author-avatar { .author-avatar {
width: 80px; width: 80px;
height: 80px; height: 80px;
@ -377,115 +504,92 @@ relatedPosts = relatedPosts.slice(0, 3);
border: 2px solid var(--accent-primary); border: 2px solid var(--accent-primary);
background-color: var(--bg-secondary); background-color: var(--bg-secondary);
} }
.author-avatar img {
.avatar-placeholder {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: bold;
color: var(--accent-primary);
} }
.author-info h3 { .author-info h3 {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
color: var(--text-primary); color: var(--text-primary);
font-size: 1.1rem; font-size: 1.2rem;
} }
.author-info p { /* Target the subtitle */
.author-info p {
color: var(--text-secondary); color: var(--text-secondary);
margin-bottom: 1rem; margin-bottom: 1rem;
font-size: 0.9rem; font-size: 0.9rem;
} }
.author-bio { /* Target the main bio */
.author-bio {
font-size: 0.9rem; font-size: 0.9rem;
margin-bottom: 0; /* Remove bottom margin */ margin-bottom: 1.5rem;
color: var(--text-secondary); color: var(--text-secondary);
text-align: left; text-align: left;
} }
/* Social links removed */
.author-links {
display: flex;
justify-content: center;
}
.author-link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(226, 232, 240, 0.05);
border-radius: 8px;
color: var(--text-primary);
text-decoration: none;
font-size: 0.9rem;
transition: all 0.3s ease;
}
.author-link:hover {
background: rgba(226, 232, 240, 0.1);
transform: translateY(-2px);
}
/* Table of Contents */
.toc-card h3 { .toc-card h3 {
margin-bottom: 1rem; margin-bottom: 1rem;
color: var(--text-primary); color: var(--text-primary);
} }
.toc-list {
list-style: none; .toc-container {
padding: 0; max-height: 500px;
margin: 0;
max-height: 60vh;
overflow-y: auto; overflow-y: auto;
} }
.toc-item {
margin-bottom: 0.9rem; /* Increased spacing */
}
.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; /* Improve readability */
}
.toc-item a:hover {
color: var(--accent-primary);
}
.toc-h3 a {
padding-left: 1.5rem; /* Increased indent */
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) { @media (max-width: 1024px) {
.blog-post-container { .blog-post-container {
grid-template-columns: 1fr; /* Stack on smaller screens */ grid-template-columns: 1fr;
} }
.blog-post-sidebar { .blog-post-sidebar {
display: none; display: none;
} }
} }
@media (max-width: 768px) {
.blog-post-title {
font-size: 1.8rem;
}
.related-posts-grid {
grid-template-columns: 1fr;
}
.blog-post {
padding: 1.5rem;
}
}
</style> </style>