576 lines
14 KiB
Plaintext
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>
|