laforceit-blog/src/layouts/BlogPost.astro

576 lines
14 KiB
Plaintext

---
import BaseLayout from './BaseLayout.astro';
import type { CollectionEntry } from 'astro:content';
type Props = {
title: string;
description: string;
pubDate: Date;
updatedDate?: Date;
heroImage?: string;
category?: string;
tags?: string[];
draft?: boolean;
};
const { title, description, pubDate, updatedDate, heroImage, category, tags, draft } = Astro.props;
// Format date with time zone
const formattedDate = pubDate ? new Date(pubDate).toLocaleDateString('en-us', {
year: 'numeric',
month: 'short',
day: 'numeric',
}) : '';
const formattedUpdatedDate = updatedDate ? new Date(updatedDate).toLocaleDateString('en-us', {
year: 'numeric',
month: 'short',
day: 'numeric',
}) : '';
// Social share URLs
const pageUrl = Astro.url.href;
const encodedUrl = encodeURIComponent(pageUrl);
const encodedTitle = encodeURIComponent(title);
const twitterShareUrl = `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`;
const linkedinShareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`;
const facebookShareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`;
---
<BaseLayout title={title} description={description}>
<div class="article-container">
<article class="blog-post">
{draft && (
<div class="draft-banner">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 20h9"></path>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
</svg>
<span>Draft Post - Content may change</span>
</div>
)}
{heroImage && <img width={1200} height={600} src={heroImage} alt="" class="hero-image" />}
<div class="post-header">
<h1 class="post-title">{title}</h1>
<div class="post-metadata">
<div class="post-info">
<time datetime={pubDate?.toISOString()} class="post-date">
<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">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
{formattedDate}
</time>
{category && (
<span class="post-category">
<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="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path>
<line x1="7" y1="7" x2="7.01" y2="7"></line>
</svg>
{category}
</span>
)}
</div>
{updatedDate && (
<div class="updated-date">
<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="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
Updated: {formattedUpdatedDate}
</div>
)}
</div>
{tags && tags.length > 0 && (
<div class="post-tags">
{tags.map(tag => (
<span class="post-tag">#{tag}</span>
))}
</div>
)}
</div>
<div class="post-content">
<slot />
</div>
<div class="post-footer">
<div class="share-post">
<span class="share-title">Share this post:</span>
<div class="share-buttons">
<a href={twitterShareUrl} target="_blank" rel="noopener noreferrer" class="share-button twitter">
<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="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path>
</svg>
</a>
<a href={linkedinShareUrl} target="_blank" rel="noopener noreferrer" class="share-button linkedin">
<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="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
<rect x="2" y="9" width="4" height="12"></rect>
<circle cx="4" cy="4" r="2"></circle>
</svg>
</a>
<a href={facebookShareUrl} target="_blank" rel="noopener noreferrer" class="share-button facebook">
<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="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
</svg>
</a>
</div>
</div>
</div>
</article>
<aside class="blog-sidebar">
<div class="sidebar-section toc">
<h3 class="sidebar-title">Table of Contents</h3>
<div class="toc-content">
<!-- Table of contents will be populated via JavaScript -->
<div id="table-of-contents"></div>
</div>
</div>
<div class="sidebar-section author">
<h3 class="sidebar-title">About the Author</h3>
<div class="author-card">
<div class="author-avatar">DL</div>
<div class="author-info">
<div class="author-name">Daniel LaForce</div>
<div class="author-bio">DevOps & Infrastructure Engineer passionate about Kubernetes, automation, and self-hosting.</div>
</div>
</div>
<div class="author-links">
<a href="https://github.com/keyargo" target="_blank" rel="noopener noreferrer" class="author-link">
<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>
<a href="https://linkedin.com/in/danlaforce" target="_blank" rel="noopener noreferrer" class="author-link">
<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="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
<rect x="2" y="9" width="4" height="12"></rect>
<circle cx="4" cy="4" r="2"></circle>
</svg>
LinkedIn
</a>
</div>
</div>
</aside>
</div>
</BaseLayout>
<style>
.article-container {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
padding: 2rem 1rem;
}
.blog-post {
background: var(--card-bg);
border-radius: 1rem;
border: 1px solid var(--card-border);
overflow: hidden;
position: relative;
}
.draft-banner {
display: flex;
align-items: center;
gap: 0.5rem;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
color: var(--bg-primary);
padding: 0.5rem 1rem;
font-weight: 500;
font-family: 'JetBrains Mono', monospace;
}
.hero-image {
width: 100%;
max-height: 400px;
object-fit: cover;
border-bottom: 1px solid var(--card-border);
}
.post-header {
padding: 2rem 2rem 1rem;
}
.post-title {
font-size: clamp(1.8rem, 4vw, 2.5rem);
line-height: 1.2;
margin-bottom: 1.5rem;
}
.post-metadata {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
font-family: 'JetBrains Mono', monospace;
color: var(--text-secondary);
}
.post-info {
display: flex;
gap: 1rem;
align-items: center;
flex-wrap: wrap;
}
.post-date, .post-category, .updated-date {
display: flex;
align-items: center;
gap: 0.5rem;
}
.post-category {
padding: 0.25rem 0.75rem;
background: rgba(6, 182, 212, 0.1);
border-radius: 2rem;
border: 1px solid rgba(6, 182, 212, 0.2);
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 1rem 0;
}
.post-tag {
padding: 0.25rem 0.75rem;
background: rgba(139, 92, 246, 0.1);
border-radius: 2rem;
border: 1px solid rgba(139, 92, 246, 0.2);
font-family: 'JetBrains Mono', monospace;
font-size: 0.875rem;
color: var(--text-secondary);
transition: all 0.3s ease;
}
.post-tag:hover {
background: rgba(139, 92, 246, 0.2);
transform: translateY(-2px);
}
.post-content {
padding: 0 2rem 2rem;
color: var(--text-secondary);
line-height: 1.8;
}
.post-content :global(h1),
.post-content :global(h2),
.post-content :global(h3),
.post-content :global(h4) {
color: var(--text-primary);
margin: 2rem 0 1rem;
}
.post-content :global(h1) {
font-size: 2rem;
}
.post-content :global(h2) {
font-size: 1.75rem;
}
.post-content :global(h3) {
font-size: 1.5rem;
}
.post-content :global(h4) {
font-size: 1.25rem;
}
.post-content :global(p) {
margin-bottom: 1.5rem;
}
.post-content :global(pre) {
background: rgba(15, 23, 42, 0.8);
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
margin: 1.5rem 0;
border: 1px solid var(--card-border);
}
.post-content :global(code) {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
}
.post-content :global(a) {
color: var(--accent-primary);
text-decoration: none;
border-bottom: 1px dashed var(--accent-primary);
transition: all 0.3s ease;
}
.post-content :global(a:hover) {
border-bottom: 1px solid var(--accent-primary);
}
.post-content :global(img) {
max-width: 100%;
border-radius: 0.5rem;
margin: 1.5rem 0;
}
.post-content :global(blockquote) {
border-left: 4px solid var(--accent-primary);
padding-left: 1rem;
font-style: italic;
margin: 1.5rem 0;
}
.post-content :global(ul),
.post-content :global(ol) {
margin: 1.5rem 0 1.5rem 1.5rem;
}
.post-content :global(li) {
margin-bottom: 0.5rem;
}
.post-footer {
padding: 1.5rem 2rem;
border-top: 1px solid var(--card-border);
}
.share-post {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 1rem;
}
.share-title {
font-weight: 500;
color: var(--text-primary);
}
.share-buttons {
display: flex;
gap: 0.75rem;
}
.share-button {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
background: rgba(226, 232, 240, 0.05);
color: var(--text-primary);
transition: all 0.3s ease;
}
.share-button:hover {
transform: translateY(-3px);
}
.share-button.twitter:hover {
background: #1DA1F2;
color: white;
}
.share-button.linkedin:hover {
background: #0077B5;
color: white;
}
.share-button.facebook:hover {
background: #1877F2;
color: white;
}
.blog-sidebar {
display: none;
}
.sidebar-section {
background: var(--card-bg);
border-radius: 1rem;
border: 1px solid var(--card-border);
padding: 1.5rem;
margin-bottom: 2rem;
}
.sidebar-title {
font-size: 1.25rem;
margin-bottom: 1rem;
color: var(--text-primary);
position: relative;
padding-bottom: 0.5rem;
}
.sidebar-title::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 50px;
height: 2px;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
}
.toc-content {
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
}
.toc-content :global(ul) {
list-style: none;
padding-left: 0;
}
.toc-content :global(li) {
margin-bottom: 0.5rem;
padding-left: 1rem;
position: relative;
}
.toc-content :global(li::before) {
content: '•';
position: absolute;
left: 0;
color: var(--accent-primary);
}
.toc-content :global(a) {
color: var(--text-secondary);
text-decoration: none;
transition: all 0.3s ease;
}
.toc-content :global(a:hover) {
color: var(--accent-primary);
}
.author-card {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
}
.author-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: var(--bg-primary);
font-size: 1.5rem;
}
.author-info {
flex: 1;
}
.author-name {
font-weight: 600;
font-size: 1.1rem;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.author-bio {
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.5;
}
.author-links {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.author-link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: rgba(226, 232, 240, 0.05);
border-radius: 0.5rem;
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);
}
@media (min-width: 768px) {
.post-content {
font-size: 1.1rem;
}
}
@media (min-width: 1024px) {
.article-container {
grid-template-columns: 1fr 300px;
gap: 2rem;
}
.blog-sidebar {
display: block;
}
}
</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>