argobox-portfolio/src/layouts/deleteBlogPostLayout.astro

645 lines
17 KiB
Plaintext

---
import BaseLayout from './BaseLayout.astro';
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
// 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 {
frontmatter: {
title: string;
description?: string;
pubDate: Date;
updatedDate?: Date;
heroImage?: string;
category?: string;
tags?: string[];
readTime?: string;
draft?: boolean;
author?: string;
// Field for explicitly related posts
related_posts?: string[];
},
slug: string // Add slug to props
}
const { frontmatter, slug } = Astro.props;
// 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',
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';
---
<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 if needed */}
{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>}
{frontmatter.category && <span class="blog-post-category">{frontmatter.category}</span>}
</div>
{/* Tags */}
{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>
)}
{/* Content Connections - Removed MiniKnowledgeGraph from here */}
{/* <div class="content-connections">
<h3 class="connections-title">Post Connections</h3>
<MiniKnowledgeGraph currentPost={currentPost} relatedPosts={relatedPosts} />
</div> */}
{/* Main Content Area */}
<div class="blog-post-content prose prose-invert max-w-none">
<slot /> {/* Renders the actual markdown content */}
</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>
{/* Sidebar */}
<aside class="blog-post-sidebar">
{/* Author Card */}
<div class="sidebar-card author-card">
<div class="author-avatar">
<div class="avatar-placeholder">DL</div>
</div>
<div class="author-info">
<h3>Daniel LaForce</h3>
<p>Infrastructure & DevOps Engineer</p>
</div>
<p class="author-bio">
Exploring enterprise-grade infrastructure, automation, Kubernetes, and self-hosted solutions for the modern home lab.
</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>
{/* MiniGraph Component - Placed after Author Card */}
<MiniGraph
slug={slug}
title={frontmatter.title}
tags={frontmatter.tags}
category={frontmatter.category}
/>
{/* Tags Section - Placed after MiniGraph */}
<div class="sidebar-card">
<h3 class="sidebar-title">Tags</h3>
<div class="tags">
{frontmatter.tags && frontmatter.tags.map(tag => (
<a href={`/tag/${tag}`} class="tag">{tag}</a>
))}
</div>
</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 Table of Contents...</p>
</nav>
</div>
</aside>
</div>
<Footer slot="footer" />
</BaseLayout>
<script>
// Table of Contents Generator
document.addEventListener('DOMContentLoaded', () => {
const tocContainer = document.getElementById('toc');
const contentArea = document.querySelector('.blog-post-content');
if (!tocContainer || !contentArea) return;
// Get all headings (h2, h3) from the content
const headings = contentArea.querySelectorAll('h2, h3');
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');
tocList.className = 'toc-list';
headings.forEach((heading, index) => {
// Add ID to heading if it doesn't have one
if (!heading.id) {
heading.id = `heading-${index}`;
}
// Create list item
const listItem = document.createElement('li');
listItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`;
// Create link
const link = document.createElement('a');
link.href = `#${heading.id}`;
link.textContent = heading.textContent;
link.addEventListener('click', (e) => {
e.preventDefault();
document.getElementById(heading.id)?.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
});
// Add to list
listItem.appendChild(link);
tocList.appendChild(listItem);
});
// Replace loading message with the TOC
tocContainer.innerHTML = '';
tocContainer.appendChild(tocList);
// Add smooth scrolling for all links pointing to headings
document.querySelectorAll('a[href^="#heading-"]').forEach(anchor => {
anchor.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href');
document.querySelector(targetId)?.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
});
});
});
</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;
}
/* Add styles for Tags in the sidebar */
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
color: var(--accent-secondary);
text-decoration: none;
font-size: 0.85rem;
transition: all 0.3s ease;
font-family: var(--font-mono);
background-color: rgba(59, 130, 246, 0.1);
padding: 0.3rem 0.6rem;
border-radius: 4px;
display: inline-block;
}
.tag:hover {
color: var(--accent-primary);
background-color: rgba(6, 182, 212, 0.15);
transform: translateY(-2px);
}
.sidebar-title {
font-size: 1.1rem;
margin-bottom: 1rem;
color: var(--text-primary);
}
</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 {
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: var(--font-mono);
}
.blog-post-title {
font-size: clamp(1.8rem, 4vw, 2.5rem);
line-height: 1.2;
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);
}
.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 {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.blog-post-tag {
color: var(--accent-secondary);
text-decoration: none;
font-size: 0.85rem;
transition: color 0.3s ease;
font-family: var(--font-mono);
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);
transform: translateY(-2px);
}
.blog-post-hero {
width: 100%;
margin-bottom: 2rem;
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;
}
/* Content Connections - Knowledge Graph */
.content-connections {
margin: 2rem 0;
padding: 1.5rem;
background: var(--bg-secondary);
border-radius: 8px;
border: 1px solid var(--card-border);
}
.connections-title {
font-size: 1.2rem;
margin-bottom: 1rem;
color: var(--text-primary);
}
/* Related Posts Section */
.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 {
position: sticky;
top: 2rem;
align-self: start;
height: calc(100vh - 4rem);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 2rem;
}
.sidebar-card {
background: var(--card-bg);
border-radius: 12px;
border: 1px solid var(--card-border);
padding: 1.5rem;
}
/* Author Card */
.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);
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: bold;
color: var(--accent-primary);
}
.author-info h3 {
margin-bottom: 0.25rem;
color: var(--text-primary);
font-size: 1.2rem;
}
.author-info p {
color: var(--text-secondary);
margin-bottom: 1rem;
font-size: 0.9rem;
}
.author-bio {
font-size: 0.9rem;
margin-bottom: 1.5rem;
color: var(--text-secondary);
text-align: left;
}
.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 {
margin-bottom: 1rem;
color: var(--text-primary);
}
.toc-container {
max-height: 500px;
overflow-y: auto;
}
@media (max-width: 1024px) {
.blog-post-container {
grid-template-columns: 1fr;
}
.blog-post-sidebar {
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>