fresh-main #7
|
@ -1,112 +0,0 @@
|
||||||
---
|
|
||||||
import '../styles/global.css';
|
|
||||||
import Header from '../components/Header.astro';
|
|
||||||
import Footer from '../components/Footer.astro';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { title, description = "LaForce IT - Home Lab & DevOps Insights" } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>{title}</title>
|
|
||||||
<meta name="description" content={description}>
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Open Graph / Social Media Meta Tags -->
|
|
||||||
<meta property="og:title" content={title} />
|
|
||||||
<meta property="og:description" content={description} />
|
|
||||||
<meta property="og:type" content="website" />
|
|
||||||
<meta property="og:url" content={Astro.url} />
|
|
||||||
<meta property="og:image" content="/blog/images/placeholders/default.jpg" />
|
|
||||||
|
|
||||||
<!-- Twitter Meta Tags -->
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
|
||||||
<meta property="twitter:domain" content="laforceit.blog" />
|
|
||||||
<meta property="twitter:url" content={Astro.url} />
|
|
||||||
<meta name="twitter:title" content={title} />
|
|
||||||
<meta name="twitter:description" content={description} />
|
|
||||||
<meta name="twitter:image" content="/blog/images/placeholders/default.jpg" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<!-- Neural network nodes - Added via JavaScript -->
|
|
||||||
<div id="neural-network"></div>
|
|
||||||
|
|
||||||
<!-- Floating shapes for background effect -->
|
|
||||||
<div class="floating-shapes">
|
|
||||||
<div class="floating-shape shape-1"></div>
|
|
||||||
<div class="floating-shape shape-2"></div>
|
|
||||||
<div class="floating-shape shape-3"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Header />
|
|
||||||
|
|
||||||
<slot />
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Create neural network nodes
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const neuralNetwork = document.getElementById('neural-network');
|
|
||||||
if (!neuralNetwork) return;
|
|
||||||
|
|
||||||
const nodeCount = Math.min(window.innerWidth / 20, 70); // Responsive node count
|
|
||||||
|
|
||||||
for (let i = 0; i < nodeCount; i++) {
|
|
||||||
const node = document.createElement('div');
|
|
||||||
node.classList.add('neural-node');
|
|
||||||
|
|
||||||
// Random position
|
|
||||||
node.style.left = `${Math.random() * 100}%`;
|
|
||||||
node.style.top = `${Math.random() * 100}%`;
|
|
||||||
|
|
||||||
// Random animation delay
|
|
||||||
node.style.animationDelay = `${Math.random() * 4}s`;
|
|
||||||
|
|
||||||
neuralNetwork.appendChild(node);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Terminal typing effect
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const terminalTyping = document.querySelector('.terminal-typing');
|
|
||||||
if (!terminalTyping) return;
|
|
||||||
|
|
||||||
const typingCommands = [
|
|
||||||
'cloudflared tunnel status',
|
|
||||||
'kubectl get pods -A',
|
|
||||||
'helm list -n monitoring',
|
|
||||||
'flux reconcile kustomization --all'
|
|
||||||
];
|
|
||||||
|
|
||||||
let currentCommandIndex = 0;
|
|
||||||
|
|
||||||
function typeCommand(command: string, element: Element, index = 0) {
|
|
||||||
if (index < command.length) {
|
|
||||||
element.textContent = command.substring(0, index + 1);
|
|
||||||
setTimeout(() => typeCommand(command, element, index + 1), 100);
|
|
||||||
} else {
|
|
||||||
// Move to next command after delay
|
|
||||||
setTimeout(() => {
|
|
||||||
currentCommandIndex = (currentCommandIndex + 1) % typingCommands.length;
|
|
||||||
typeCommand(typingCommands[currentCommandIndex], element, 0);
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
typeCommand(typingCommands[currentCommandIndex], terminalTyping);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,108 +0,0 @@
|
||||||
---
|
|
||||||
import { getCollection } from 'astro:content';
|
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
|
||||||
|
|
||||||
// Get all blog entries
|
|
||||||
const allPosts = await getCollection('blog');
|
|
||||||
|
|
||||||
// Sort by publication date
|
|
||||||
const sortedPosts = allPosts.sort((a, b) => {
|
|
||||||
const dateA = a.data.pubDate ? new Date(a.data.pubDate) : new Date(0);
|
|
||||||
const dateB = b.data.pubDate ? new Date(b.data.pubDate) : new Date(0);
|
|
||||||
return dateB.getTime() - dateA.getTime();
|
|
||||||
});
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title="Blog | LaForce IT - Home Lab & DevOps Insights" description="Explore articles about Kubernetes, Infrastructure, DevOps, and Home Lab setups">
|
|
||||||
<main class="container">
|
|
||||||
<section class="blog-header">
|
|
||||||
<h1 class="blog-title">Blog</h1>
|
|
||||||
<p class="blog-description">
|
|
||||||
Technical insights, infrastructure guides, and DevOps best practices from my home lab to production environments.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="blog-grid">
|
|
||||||
{sortedPosts.map((post) => (
|
|
||||||
<article class="post-card">
|
|
||||||
{post.data.heroImage ? (
|
|
||||||
<img
|
|
||||||
width={720}
|
|
||||||
height={360}
|
|
||||||
src={post.data.heroImage}
|
|
||||||
alt=""
|
|
||||||
class="post-image"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
width={720}
|
|
||||||
height={360}
|
|
||||||
src="/blog/images/placeholders/default.jpg"
|
|
||||||
alt=""
|
|
||||||
class="post-image"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div class="post-content">
|
|
||||||
<div class="post-meta">
|
|
||||||
<time datetime={post.data.pubDate ? new Date(post.data.pubDate).toISOString() : ''}>
|
|
||||||
{post.data.pubDate ? new Date(post.data.pubDate).toLocaleDateString('en-us', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
}) : 'No date'}
|
|
||||||
</time>
|
|
||||||
{post.data.category && (
|
|
||||||
<span class="post-category">
|
|
||||||
{post.data.category}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<h3 class="post-title">
|
|
||||||
<a href={`/blog/${post.slug}/`}>{post.data.title}</a>
|
|
||||||
{post.data.draft && <span class="ml-2 px-2 py-1 bg-gray-200 text-gray-700 text-xs rounded">Draft</span>}
|
|
||||||
</h3>
|
|
||||||
<p class="post-excerpt">{post.data.description}</p>
|
|
||||||
<div class="post-footer">
|
|
||||||
<span class="post-read-time">{post.data.readTime || '5 min read'}</span>
|
|
||||||
<a href={`/blog/${post.slug}/`} class="read-more">Read More</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.blog-header {
|
|
||||||
margin: 3rem 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-title {
|
|
||||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-tertiary));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-description {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: clamp(1rem, 2vw, 1.2rem);
|
|
||||||
max-width: 700px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-grid {
|
|
||||||
margin: 2rem 0 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.blog-header {
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,832 +0,0 @@
|
||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--bg-primary: #050a18;
|
|
||||||
--bg-secondary: #0d1529;
|
|
||||||
--text-primary: #e2e8f0;
|
|
||||||
--text-secondary: #94a3b8;
|
|
||||||
--accent-primary: #06b6d4;
|
|
||||||
--accent-secondary: #3b82f6;
|
|
||||||
--accent-tertiary: #8b5cf6;
|
|
||||||
--glow-primary: rgba(6, 182, 212, 0.3);
|
|
||||||
--glow-secondary: rgba(59, 130, 246, 0.3);
|
|
||||||
--card-bg: rgba(15, 23, 42, 0.8);
|
|
||||||
--card-border: rgba(56, 189, 248, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Space Grotesk', sans-serif;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1.6;
|
|
||||||
overflow-x: hidden;
|
|
||||||
background-image:
|
|
||||||
radial-gradient(circle at 20% 35%, rgba(6, 182, 212, 0.05) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 75% 15%, rgba(59, 130, 246, 0.05) 0%, transparent 45%),
|
|
||||||
radial-gradient(circle at 85% 70%, rgba(139, 92, 246, 0.05) 0%, transparent 40%);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Grid overlay effect */
|
|
||||||
body::before {
|
|
||||||
content: "";
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(rgba(226, 232, 240, 0.03) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, rgba(226, 232, 240, 0.03) 1px, transparent 1px);
|
|
||||||
background-size: 30px 30px;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Neural network nodes */
|
|
||||||
.neural-node {
|
|
||||||
position: fixed;
|
|
||||||
width: 2px;
|
|
||||||
height: 2px;
|
|
||||||
background: rgba(226, 232, 240, 0.2);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: pulse 4s infinite alternate ease-in-out;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1.5);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Terminal cursor animation */
|
|
||||||
@keyframes blink {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header styles */
|
|
||||||
header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 1.5rem clamp(1rem, 5%, 3rem);
|
|
||||||
position: relative;
|
|
||||||
background: linear-gradient(180deg, var(--bg-secondary), transparent);
|
|
||||||
border-bottom: 1px solid rgba(56, 189, 248, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo span {
|
|
||||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-symbol {
|
|
||||||
width: 2.5rem;
|
|
||||||
height: 2.5rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
color: var(--bg-primary);
|
|
||||||
box-shadow: 0 0 15px var(--glow-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
display: flex;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
width: 0;
|
|
||||||
height: 2px;
|
|
||||||
bottom: -5px;
|
|
||||||
left: 0;
|
|
||||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
|
||||||
transition: width 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a:hover::after {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-btn {
|
|
||||||
display: none;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Floating shapes */
|
|
||||||
.floating-shapes {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-shape {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.05;
|
|
||||||
filter: blur(30px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.shape-1 {
|
|
||||||
width: 300px;
|
|
||||||
height: 300px;
|
|
||||||
background: var(--accent-primary);
|
|
||||||
top: 20%;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shape-2 {
|
|
||||||
width: 200px;
|
|
||||||
height: 200px;
|
|
||||||
background: var(--accent-secondary);
|
|
||||||
bottom: 10%;
|
|
||||||
left: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.shape-3 {
|
|
||||||
width: 150px;
|
|
||||||
height: 150px;
|
|
||||||
background: var(--accent-tertiary);
|
|
||||||
top: 70%;
|
|
||||||
right: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blog post cards */
|
|
||||||
.post-card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid var(--card-border);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 10px 30px rgba(6, 182, 212, 0.1);
|
|
||||||
border-color: rgba(56, 189, 248, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: linear-gradient(135deg, rgba(6, 182, 212, 0.05), rgba(139, 92, 246, 0.05));
|
|
||||||
z-index: -1;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-card:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-image {
|
|
||||||
width: 100%;
|
|
||||||
height: 200px;
|
|
||||||
object-fit: cover;
|
|
||||||
border-bottom: 1px solid var(--card-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-content {
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-meta {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-category {
|
|
||||||
background: rgba(6, 182, 212, 0.1);
|
|
||||||
color: var(--accent-primary);
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-title a {
|
|
||||||
color: var(--text-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-title a:hover {
|
|
||||||
color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-excerpt {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-more {
|
|
||||||
color: var(--accent-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-more:hover {
|
|
||||||
color: var(--accent-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-more::after {
|
|
||||||
content: '→';
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section styles */
|
|
||||||
.section-title {
|
|
||||||
font-size: clamp(1.5rem, 3vw, 2.5rem);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
height: 4px;
|
|
||||||
width: 60px;
|
|
||||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
|
||||||
bottom: -10px;
|
|
||||||
left: 0;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer styles */
|
|
||||||
footer {
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
padding: 3rem clamp(1rem, 5%, 3rem);
|
|
||||||
position: relative;
|
|
||||||
border-top: 1px solid rgba(56, 189, 248, 0.1);
|
|
||||||
margin-top: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-col h4 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links li {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links a {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links a:hover {
|
|
||||||
color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: rgba(226, 232, 240, 0.05);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link:hover {
|
|
||||||
background: var(--accent-primary);
|
|
||||||
color: var(--bg-primary);
|
|
||||||
transform: translateY(-3px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom {
|
|
||||||
text-align: center;
|
|
||||||
padding-top: 2rem;
|
|
||||||
border-top: 1px solid rgba(226, 232, 240, 0.05);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom a {
|
|
||||||
color: var(--accent-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hero section for homepage */
|
|
||||||
.hero {
|
|
||||||
min-height: 80vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 3rem clamp(1rem, 5%, 3rem);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content {
|
|
||||||
max-width: 650px;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-subtitle {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
color: var(--accent-primary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-subtitle::before {
|
|
||||||
content: '>';
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title {
|
|
||||||
font-size: clamp(2.5rem, 5vw, 4rem);
|
|
||||||
line-height: 1.1;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title span {
|
|
||||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-tertiary));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-description {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
max-width: 85%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
|
||||||
color: var(--bg-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 0 20px var(--glow-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 0 30px var(--glow-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Terminal box */
|
|
||||||
.terminal-box {
|
|
||||||
width: 100%;
|
|
||||||
background: var(--bg-secondary);
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 1px solid var(--card-border);
|
|
||||||
box-shadow: 0 0 30px rgba(6, 182, 212, 0.1);
|
|
||||||
padding: 1.5rem;
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
z-index: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
border-bottom: 1px solid rgba(226, 232, 240, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-dots {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-dot {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-dot-red {
|
|
||||||
background: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-dot-yellow {
|
|
||||||
background: #eab308;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-dot-green {
|
|
||||||
background: #22c55e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-title {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-content {
|
|
||||||
flex: 1;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-line {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-prompt {
|
|
||||||
color: var(--accent-primary);
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-command {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-output {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-typing {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-typing::after {
|
|
||||||
content: '|';
|
|
||||||
position: absolute;
|
|
||||||
right: -10px;
|
|
||||||
animation: blink 1s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Container and content layout */
|
|
||||||
.container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
main {
|
|
||||||
padding: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blog content styling */
|
|
||||||
.blog-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Digital Garden */
|
|
||||||
.digital-garden-intro {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 1024px) {
|
|
||||||
.hero {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
header {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-description {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title {
|
|
||||||
font-size: clamp(1.5rem, 6vw, 2.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-box {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 300px;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-card {
|
|
||||||
min-height: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-metadata {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-info {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-tags {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add mobile menu functionality */
|
|
||||||
.mobile-menu-btn {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.desktop-nav {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.mobile-nav-active {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100vh;
|
|
||||||
background: var(--bg-primary);
|
|
||||||
z-index: 1000;
|
|
||||||
padding: 2rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav.mobile-nav-active a {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-menu-close {
|
|
||||||
align-self: flex-end;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-size: 1.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.mobile-menu-btn {
|
|
||||||
display: block;
|
|
||||||
position: absolute;
|
|
||||||
right: 1.5rem;
|
|
||||||
top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Additional mobile optimizations for very small screens */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
:root {
|
|
||||||
--container-padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-title {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-card {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-image {
|
|
||||||
height: 160px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-subtitle {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button {
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-content {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Adjust footer layout */
|
|
||||||
.footer-col {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Touch device optimizations */
|
|
||||||
@media (hover: none) {
|
|
||||||
.post-card:hover {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-button:hover {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav a:hover::after {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link:hover {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-tag:hover {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Increase tap target sizes */
|
|
||||||
nav a {
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links li {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links a {
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-footer {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,566 +0,0 @@
|
||||||
---
|
|
||||||
import { getCollection } from 'astro:content';
|
|
||||||
import type { CollectionEntry } from 'astro:content';
|
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
|
||||||
import DigitalGardenGraph from '../components/DigitalGardenGraph.astro';
|
|
||||||
|
|
||||||
type Post = CollectionEntry<'posts'>;
|
|
||||||
type Config = CollectionEntry<'configurations'>;
|
|
||||||
type Project = CollectionEntry<'projects'>;
|
|
||||||
|
|
||||||
// Get all blog posts (excluding configurations and specific guides)
|
|
||||||
const posts = (await getCollection('blog'))
|
|
||||||
.filter(item =>
|
|
||||||
!item.slug.startsWith('configurations/') &&
|
|
||||||
!item.slug.startsWith('projects/') &&
|
|
||||||
!item.data.category?.toLowerCase().includes('configuration') &&
|
|
||||||
!item.slug.includes('setup-guide') &&
|
|
||||||
!item.slug.includes('config')
|
|
||||||
)
|
|
||||||
.sort((a, b) => new Date(b.data.pubDate || 0).valueOf() - new Date(a.data.pubDate || 0).valueOf());
|
|
||||||
|
|
||||||
// Get configuration posts
|
|
||||||
const configurations = (await getCollection('blog'))
|
|
||||||
.filter(item =>
|
|
||||||
item.slug.startsWith('configurations/') ||
|
|
||||||
item.data.category?.toLowerCase().includes('configuration') ||
|
|
||||||
item.slug.includes('setup-guide') ||
|
|
||||||
item.slug.includes('config') ||
|
|
||||||
item.slug.includes('monitoring') ||
|
|
||||||
item.slug.includes('server') ||
|
|
||||||
item.slug.includes('tunnel')
|
|
||||||
)
|
|
||||||
.sort((a, b) => new Date(b.data.pubDate || 0).valueOf() - new Date(a.data.pubDate || 0).valueOf());
|
|
||||||
|
|
||||||
// Get project posts
|
|
||||||
const projects = (await getCollection('blog'))
|
|
||||||
.filter(item =>
|
|
||||||
item.slug.startsWith('projects/') ||
|
|
||||||
item.data.category?.toLowerCase().includes('project')
|
|
||||||
)
|
|
||||||
.sort((a, b) => new Date(b.data.pubDate || 0).valueOf() - new Date(a.data.pubDate || 0).valueOf());
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title="LaForce IT - Home Lab & DevOps Insights">
|
|
||||||
<!-- Hero section -->
|
|
||||||
<section class="hero">
|
|
||||||
<div class="hero-content">
|
|
||||||
<div class="hero-subtitle">Home Lab & DevOps</div>
|
|
||||||
<h1 class="hero-title">Exploring <span>advanced infrastructure</span> and automation</h1>
|
|
||||||
<p class="hero-description">
|
|
||||||
Join me on a journey through enterprise-grade home lab setups, Kubernetes deployments, and DevOps best practices for the modern tech enthusiast.
|
|
||||||
</p>
|
|
||||||
<div class="social-links-hero">
|
|
||||||
<a href="https://github.com/keyargo" target="_blank" rel="noopener noreferrer" class="social-link-hero github">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
|
|
||||||
</a>
|
|
||||||
<a href="https://linkedin.com/in/danlaforce" target="_blank" rel="noopener noreferrer" class="social-link-hero linkedin">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
|
|
||||||
</div>
|
|
||||||
<a href="#posts" class="cta-button">
|
|
||||||
Explore Latest Posts
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="terminal-box">
|
|
||||||
<div class="terminal-header">
|
|
||||||
<div class="terminal-dots">
|
|
||||||
<div class="terminal-dot terminal-dot-red"></div>
|
|
||||||
<div class="terminal-dot terminal-dot-yellow"></div>
|
|
||||||
<div class="terminal-dot terminal-dot-green"></div>
|
|
||||||
</div>
|
|
||||||
<div class="terminal-title">argobox:~/homelab</div>
|
|
||||||
</div>
|
|
||||||
<div class="terminal-content">
|
|
||||||
<div class="terminal-line">
|
|
||||||
<span class="terminal-prompt">$</span>
|
|
||||||
<span class="terminal-command">kubectl get nodes</span>
|
|
||||||
</div>
|
|
||||||
<div class="terminal-output">
|
|
||||||
NAME STATUS ROLES AGE VERSION<br>
|
|
||||||
argobox Ready <none> 47d v1.28.3+k3s1<br>
|
|
||||||
argobox-lite Ready control-plane,master 47d v1.28.3+k3s1
|
|
||||||
</div>
|
|
||||||
<div class="terminal-line">
|
|
||||||
<span class="terminal-prompt">$</span>
|
|
||||||
<span class="terminal-command">helm list -A</span>
|
|
||||||
</div>
|
|
||||||
<div class="terminal-output">
|
|
||||||
NAME NAMESPACE REVISION STATUS CHART<br>
|
|
||||||
cloudnative-pg postgres 1 deployed cloudnative-pg-0.18.0<br>
|
|
||||||
prometheus monitoring 2 deployed kube-prometheus-stack-51.2.0
|
|
||||||
</div>
|
|
||||||
<div class="terminal-line">
|
|
||||||
<span class="terminal-prompt">$</span>
|
|
||||||
<span class="terminal-command terminal-typing">cloudflared tunnel status</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Digital Garden Visualization -->
|
|
||||||
<section class="container">
|
|
||||||
<h2 class="section-title">My Digital Garden</h2>
|
|
||||||
<p class="digital-garden-intro">
|
|
||||||
This blog functions as my personal digital garden - a collection of interconnected ideas, guides, and projects.
|
|
||||||
Browse through the visualization below to see how different concepts relate to each other.
|
|
||||||
</p>
|
|
||||||
<DigitalGardenGraph />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Main content sections -->
|
|
||||||
<main class="container">
|
|
||||||
<section id="posts" class="mb-16">
|
|
||||||
<h2 class="section-title">Latest Posts</h2>
|
|
||||||
<div class="blog-grid">
|
|
||||||
{posts.map((post) => (
|
|
||||||
<article class="post-card">
|
|
||||||
{post.data.heroImage ? (
|
|
||||||
<img
|
|
||||||
width={720}
|
|
||||||
height={360}
|
|
||||||
src={post.data.heroImage}
|
|
||||||
alt=""
|
|
||||||
class="post-image"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
width={720}
|
|
||||||
height={360}
|
|
||||||
src="/blog/images/placeholders/default.jpg"
|
|
||||||
alt=""
|
|
||||||
class="post-image"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div class="post-content">
|
|
||||||
<div class="post-meta">
|
|
||||||
<time datetime={post.data.pubDate ? new Date(post.data.pubDate).toISOString() : ''}>
|
|
||||||
{post.data.pubDate ? new Date(post.data.pubDate).toLocaleDateString('en-us', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
}) : 'No date'}
|
|
||||||
</time>
|
|
||||||
{post.data.category && (
|
|
||||||
<span class="post-category">
|
|
||||||
{post.data.category}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<h3 class="post-title">
|
|
||||||
<a href={`/blog/${post.slug}/`}>{post.data.title}</a>
|
|
||||||
{post.data.draft && <span class="ml-2 px-2 py-1 bg-gray-200 text-gray-700 text-xs rounded">Draft</span>}
|
|
||||||
</h3>
|
|
||||||
<p class="post-excerpt">{post.data.description}</p>
|
|
||||||
<div class="post-footer">
|
|
||||||
<span class="post-read-time">{post.data.readTime || '5 min read'}</span>
|
|
||||||
<a href={`/blog/${post.slug}/`} class="read-more">Read More</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="configurations" class="mb-16">
|
|
||||||
<h2 class="section-title">Configurations</h2>
|
|
||||||
<div class="blog-grid">
|
|
||||||
{configurations.map((config) => (
|
|
||||||
<article class="post-card">
|
|
||||||
{config.data.heroImage ? (
|
|
||||||
<img
|
|
||||||
width={720}
|
|
||||||
height={360}
|
|
||||||
src={config.data.heroImage}
|
|
||||||
alt=""
|
|
||||||
class="post-image"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
width={720}
|
|
||||||
height={360}
|
|
||||||
src="/blog/images/placeholders/default.jpg"
|
|
||||||
alt=""
|
|
||||||
class="post-image"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div class="post-content">
|
|
||||||
<div class="post-meta">
|
|
||||||
<time datetime={config.data.pubDate ? new Date(config.data.pubDate).toISOString() : ''}>
|
|
||||||
{config.data.pubDate ? new Date(config.data.pubDate).toLocaleDateString('en-us', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
}) : 'No date'}
|
|
||||||
</time>
|
|
||||||
{config.data.category && (
|
|
||||||
<span class="post-category">
|
|
||||||
{config.data.category}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<h3 class="post-title">
|
|
||||||
<a href={`/blog/${config.slug}/`}>{config.data.title}</a>
|
|
||||||
{config.data.draft && <span class="ml-2 px-2 py-1 bg-gray-200 text-gray-700 text-xs rounded">Draft</span>}
|
|
||||||
</h3>
|
|
||||||
<p class="post-excerpt">{config.data.description}</p>
|
|
||||||
<div class="post-footer">
|
|
||||||
<span class="post-read-time">{config.data.readTime || '5 min read'}</span>
|
|
||||||
<a href={`/blog/${config.slug}/`} class="read-more">Read More</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section id="projects" class="mb-16">
|
|
||||||
<h2 class="section-title">Projects</h2>
|
|
||||||
<div class="blog-grid">
|
|
||||||
{projects.map((project) => (
|
|
||||||
<article class="post-card">
|
|
||||||
{project.data.heroImage ? (
|
|
||||||
<img
|
|
||||||
width={720}
|
|
||||||
height={360}
|
|
||||||
src={project.data.heroImage}
|
|
||||||
alt=""
|
|
||||||
class="post-image"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
width={720}
|
|
||||||
height={360}
|
|
||||||
src="/blog/images/placeholders/default.jpg"
|
|
||||||
alt=""
|
|
||||||
class="post-image"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div class="post-content">
|
|
||||||
<div class="post-meta">
|
|
||||||
<time datetime={project.data.pubDate ? new Date(project.data.pubDate).toISOString() : ''}>
|
|
||||||
{project.data.pubDate ? new Date(project.data.pubDate).toLocaleDateString('en-us', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
}) : 'No date'}
|
|
||||||
</time>
|
|
||||||
{project.data.category && (
|
|
||||||
<span class="post-category">
|
|
||||||
{project.data.category}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<h3 class="post-title">
|
|
||||||
<a href={`/blog/${project.slug}/`}>{project.data.title}</a>
|
|
||||||
{project.data.draft && <span class="ml-2 px-2 py-1 bg-gray-200 text-gray-700 text-xs rounded">Draft</span>}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
{project.data.technologies && (
|
|
||||||
<div class="mb-2 flex flex-wrap gap-2">
|
|
||||||
{project.data.technologies.map((tech) => (
|
|
||||||
<span class="post-category">
|
|
||||||
{tech}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<p class="post-excerpt">{project.data.description}</p>
|
|
||||||
<div class="post-footer">
|
|
||||||
<div class="flex gap-4">
|
|
||||||
{project.data.github && (
|
|
||||||
<a href={project.data.github} target="_blank" rel="noopener noreferrer" class="read-more">
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{project.data.live && (
|
|
||||||
<a href={project.data.live} target="_blank" rel="noopener noreferrer" class="read-more">
|
|
||||||
Live Demo
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<a href={`/blog/${project.slug}/`} class="read-more">View Project</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Featured section -->
|
|
||||||
<section class="featured-section">
|
|
||||||
<div class="featured-grid">
|
|
||||||
<div class="featured-content">
|
|
||||||
<div class="featured-subtitle">Featured Project</div>
|
|
||||||
<h2 class="featured-title">ArgoBox <span>Home Lab Architecture</span></h2>
|
|
||||||
<p class="featured-description">
|
|
||||||
A complete enterprise-grade home infrastructure built on Kubernetes, featuring high availability, zero-trust networking, and fully automated deployments.
|
|
||||||
</p>
|
|
||||||
<ul class="featured-list">
|
|
||||||
<li class="featured-list-item">
|
|
||||||
<div class="featured-list-icon">✓</div>
|
|
||||||
<div>Multi-node K3s cluster with automatic failover</div>
|
|
||||||
</li>
|
|
||||||
<li class="featured-list-item">
|
|
||||||
<div class="featured-list-icon">✓</div>
|
|
||||||
<div>Gitea + Flux CD for GitOps-based continuous deployment</div>
|
|
||||||
</li>
|
|
||||||
<li class="featured-list-item">
|
|
||||||
<div class="featured-list-icon">✓</div>
|
|
||||||
<div>Cloudflare Tunnels for secure, zero-trust remote access</div>
|
|
||||||
</li>
|
|
||||||
<li class="featured-list-item">
|
|
||||||
<div class="featured-list-icon">✓</div>
|
|
||||||
<div>Synology NAS integration with Kubernetes volumes</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<a href="#" class="cta-button">
|
|
||||||
View Project Details
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- About Me Section -->
|
|
||||||
<section class="about-section mb-16">
|
|
||||||
<h2 class="section-title">About Me</h2>
|
|
||||||
<div class="about-content">
|
|
||||||
<div class="about-text">
|
|
||||||
<p>
|
|
||||||
Hi, I'm Daniel LaForce, a passionate DevOps and infrastructure engineer with a focus on Kubernetes,
|
|
||||||
automation, and cloud technologies. When I'm not working on enterprise systems, I'm building and
|
|
||||||
refining my home lab environment to test and learn new technologies.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
This site serves as both my technical blog and digital garden - a place to share what I've learned
|
|
||||||
and document my ongoing projects. Feel free to connect with me on GitHub or LinkedIn!
|
|
||||||
</p>
|
|
||||||
<div class="social-links">
|
|
||||||
<a href="https://github.com/keyargo" target="_blank" rel="noopener noreferrer" class="social-link">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
|
|
||||||
<span>GitHub</span>
|
|
||||||
</a>
|
|
||||||
<a href="https://linkedin.com/in/danlaforce" target="_blank" rel="noopener noreferrer" class="social-link">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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>
|
|
||||||
<span>LinkedIn</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.featured-section {
|
|
||||||
margin-top: 4rem;
|
|
||||||
background: var(--card-bg);
|
|
||||||
border-radius: 1rem;
|
|
||||||
border: 1px solid var(--card-border);
|
|
||||||
padding: 2rem;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-subtitle {
|
|
||||||
font-family: 'JetBrains Mono', monospace;
|
|
||||||
color: var(--accent-primary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
letter-spacing: 2px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-title {
|
|
||||||
font-size: clamp(1.8rem, 4vw, 2.5rem);
|
|
||||||
line-height: 1.2;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-title span {
|
|
||||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-tertiary));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-description {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
max-width: 600px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-list {
|
|
||||||
list-style: none;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-list-item {
|
|
||||||
display: flex;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.featured-list-icon {
|
|
||||||
color: var(--accent-primary);
|
|
||||||
margin-right: 1rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mb-16 {
|
|
||||||
margin-bottom: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-wrap {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-2 {
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-4 {
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ml-2 {
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.px-2 {
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.py-1 {
|
|
||||||
padding-top: 0.25rem;
|
|
||||||
padding-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-gray-200 {
|
|
||||||
background-color: rgba(226, 232, 240, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-gray-700 {
|
|
||||||
color: #94a3b8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-xs {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rounded {
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links-hero {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link-hero {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--card-bg);
|
|
||||||
color: var(--text-primary);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
border: 1px solid var(--card-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link-hero:hover {
|
|
||||||
transform: translateY(-3px);
|
|
||||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link-hero.github:hover {
|
|
||||||
background-color: #24292e;
|
|
||||||
border-color: #24292e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link-hero.linkedin:hover {
|
|
||||||
background-color: #0077b5;
|
|
||||||
border-color: #0077b5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-section {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border-radius: 1rem;
|
|
||||||
border: 1px solid var(--card-border);
|
|
||||||
padding: 2rem;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-text {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-text p {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-links {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.5rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
color: var(--text-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background-color: rgba(226, 232, 240, 0.05);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link:hover {
|
|
||||||
background-color: rgba(226, 232, 240, 0.1);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.featured-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-content {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.about-content {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
// Footer.astro
|
// src/components/Footer.astro
|
||||||
// High-quality footer with navigation, social links and additional elements
|
// High-quality footer with navigation, social links and additional elements
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
@ -9,11 +9,11 @@ const categories = [
|
||||||
{
|
{
|
||||||
title: 'Technology',
|
title: 'Technology',
|
||||||
links: [
|
links: [
|
||||||
{ name: 'Kubernetes', path: '/blog/category/kubernetes' },
|
{ name: 'Kubernetes', path: '/categories/kubernetes' },
|
||||||
{ name: 'Docker', path: '/blog/category/docker' },
|
{ name: 'Docker', path: '/categories/docker' },
|
||||||
{ name: 'DevOps', path: '/blog/category/devops' },
|
{ name: 'DevOps', path: '/categories/devops' },
|
||||||
{ name: 'Networking', path: '/blog/category/networking' },
|
{ name: 'Networking', path: '/categories/networking' },
|
||||||
{ name: 'Storage', path: '/blog/category/storage' }
|
{ name: 'Storage', path: '/categories/storage' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -29,8 +29,8 @@ const categories = [
|
||||||
{
|
{
|
||||||
title: 'Projects',
|
title: 'Projects',
|
||||||
links: [
|
links: [
|
||||||
{ name: 'HomeLab Setup', path: '/projects/homelab' },
|
{ name: 'HomeLab Setup', url: 'https://argobox.com' },
|
||||||
{ name: 'Tech Stack', path: '/projects/tech-stack' },
|
{ name: 'Tech Stack', url: 'https://argobox.com/#services' },
|
||||||
{ name: 'Github Repos', path: '/projects/github' },
|
{ name: 'Github Repos', path: '/projects/github' },
|
||||||
{ name: 'Live Services', path: '/projects/services' },
|
{ name: 'Live Services', path: '/projects/services' },
|
||||||
{ name: 'Obsidian Templates', path: '/projects/obsidian' }
|
{ name: 'Obsidian Templates', path: '/projects/obsidian' }
|
||||||
|
@ -42,7 +42,7 @@ const categories = [
|
||||||
const socialLinks = [
|
const socialLinks = [
|
||||||
{
|
{
|
||||||
name: 'GitHub',
|
name: 'GitHub',
|
||||||
url: 'https://github.com/yourusername',
|
url: 'https://github.com/KeyArgo/',
|
||||||
icon: '<path fill-rule="evenodd" clip-rule="evenodd" d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385c.6.105.825-.255.825-.57c0-.285-.015-1.23-.015-2.235c-3.015.555-3.795-.735-4.035-1.41c-.135-.345-.72-1.41-1.23-1.695c-.42-.225-1.02-.78-.015-.795c.945-.015 1.62.87 1.845 1.23c1.08 1.815 2.805 1.305 3.495.99c.105-.78.42-1.305.765-1.605c-2.67-.3-5.46-1.335-5.46-5.925c0-1.305.465-2.385 1.23-3.225c-.12-.3-.54-1.53.12-3.18c0 0 1.005-.315 3.3 1.23c.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23c.66 1.65.24 2.88.12 3.18c.765.84 1.23 1.905 1.23 3.225c0 4.605-2.805 5.625-5.475 5.925c.435.375.81 1.095.81 2.22c0 1.605-.015 2.895-.015 3.3c0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />'
|
icon: '<path fill-rule="evenodd" clip-rule="evenodd" d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385c.6.105.825-.255.825-.57c0-.285-.015-1.23-.015-2.235c-3.015.555-3.795-.735-4.035-1.41c-.135-.345-.72-1.41-1.23-1.695c-.42-.225-1.02-.78-.015-.795c.945-.015 1.62.87 1.845 1.23c1.08 1.815 2.805 1.305 3.495.99c.105-.78.42-1.305.765-1.605c-2.67-.3-5.46-1.335-5.46-5.925c0-1.305.465-2.385 1.23-3.225c-.12-.3-.54-1.53.12-3.18c0 0 1.005-.315 3.3 1.23c.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23c.66 1.65.24 2.88.12 3.18c.765.84 1.23 1.905 1.23 3.225c0 4.605-2.805 5.625-5.475 5.925c.435.375.81 1.095.81 2.22c0 1.605-.015 2.895-.015 3.3c0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z" />'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -130,7 +130,14 @@ const services = [
|
||||||
<ul class="footer-links">
|
<ul class="footer-links">
|
||||||
{category.links.map(link => (
|
{category.links.map(link => (
|
||||||
<li>
|
<li>
|
||||||
<a href={link.path} class="footer-link">{link.name}</a>
|
<a
|
||||||
|
href={link.url || link.path}
|
||||||
|
class="footer-link"
|
||||||
|
target={link.url ? "_blank" : undefined}
|
||||||
|
rel={link.url ? "noopener noreferrer" : undefined}
|
||||||
|
>
|
||||||
|
{link.name}
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -423,9 +423,9 @@ const currentPath = Astro.url.pathname;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search functionality - client-side post filtering
|
// Search functionality - client-side site-wide filtering (User provided version)
|
||||||
const searchResults = document.getElementById('search-results');
|
const searchResults = document.getElementById('search-results'); // Assuming this ID exists in your dropdown HTML
|
||||||
|
|
||||||
// Function to perform search
|
// Function to perform search
|
||||||
const performSearch = async (query) => {
|
const performSearch = async (query) => {
|
||||||
if (!query || query.length < 2) {
|
if (!query || query.length < 2) {
|
||||||
|
@ -437,39 +437,68 @@ const currentPath = Astro.url.pathname;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This would ideally be a server-side search or a pre-built index
|
// Fetch the search index that contains all site content
|
||||||
// For now, we'll just fetch all posts and filter client-side
|
const response = await fetch('/search-index.json'); // Ensure this path is correct based on your build output
|
||||||
const response = await fetch('/search-index.json');
|
|
||||||
if (!response.ok) throw new Error('Failed to fetch search data');
|
if (!response.ok) throw new Error('Failed to fetch search data');
|
||||||
|
|
||||||
const posts = await response.json();
|
const allContent = await response.json();
|
||||||
const results = posts.filter(post => {
|
const results = allContent.filter(item => {
|
||||||
const lowerQuery = query.toLowerCase();
|
const lowerQuery = query.toLowerCase();
|
||||||
return (
|
return (
|
||||||
post.title.toLowerCase().includes(lowerQuery) ||
|
item.title.toLowerCase().includes(lowerQuery) ||
|
||||||
post.description?.toLowerCase().includes(lowerQuery) ||
|
item.description?.toLowerCase().includes(lowerQuery) ||
|
||||||
post.tags?.some(tag => tag.toLowerCase().includes(lowerQuery))
|
item.tags?.some(tag => tag.toLowerCase().includes(lowerQuery)) ||
|
||||||
|
item.category?.toLowerCase().includes(lowerQuery)
|
||||||
);
|
);
|
||||||
}).slice(0, 5); // Limit to 5 results
|
}).slice(0, 8); // Limit to 8 results for better UI
|
||||||
|
|
||||||
// Display results
|
// Display results
|
||||||
if (searchResults) {
|
if (searchResults) {
|
||||||
if (results.length > 0) {
|
if (results.length > 0) {
|
||||||
searchResults.innerHTML = results.map(post => `
|
searchResults.innerHTML = results.map(item => {
|
||||||
<div class="search-result-item" data-url="/posts/${post.slug}/">
|
// Create type badge
|
||||||
<div class="search-result-title">${post.title}</div>
|
let typeBadge = '';
|
||||||
<div class="search-result-snippet">${post.description || ''}</div>
|
switch(item.type) {
|
||||||
</div>
|
case 'post':
|
||||||
`).join('');
|
typeBadge = '<span class="result-type post">Blog</span>';
|
||||||
|
break;
|
||||||
|
case 'project':
|
||||||
|
typeBadge = '<span class="result-type project">Project</span>';
|
||||||
|
break;
|
||||||
|
case 'configuration':
|
||||||
|
typeBadge = '<span class="result-type config">Config</span>';
|
||||||
|
break;
|
||||||
|
case 'external':
|
||||||
|
typeBadge = '<span class="result-type external">External</span>';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
typeBadge = '<span class="result-type">Content</span>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="search-result-item" data-url="${item.url}">
|
||||||
|
<div class="search-result-header">
|
||||||
|
<div class="search-result-title">${item.title}</div>
|
||||||
|
${typeBadge}
|
||||||
|
</div>
|
||||||
|
<div class="search-result-snippet">${item.description || ''}</div>
|
||||||
|
${item.tags && item.tags.length > 0 ?
|
||||||
|
`<div class="search-result-tags">
|
||||||
|
${item.tags.slice(0, 3).map(tag => `<span class="search-tag">${tag}</span>`).join('')}
|
||||||
|
</div>` : ''
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
// Add click handlers to results
|
// Add click handlers to results
|
||||||
document.querySelectorAll('.search-result-item').forEach(item => {
|
document.querySelectorAll('.search-result-item').forEach(item => {
|
||||||
item.addEventListener('click', () => {
|
item.addEventListener('click', () => {
|
||||||
window.location.href = item.dataset.url;
|
window.location.href = item.dataset.url; // Navigate to the item's URL
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
searchResults.innerHTML = '<div class="no-results">No matching posts found</div>';
|
searchResults.innerHTML = '<div class="no-results">No matching content found</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -479,8 +508,8 @@ const currentPath = Astro.url.pathname;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Search input event handler
|
// Search input event handler with debounce
|
||||||
let searchTimeout;
|
let searchTimeout;
|
||||||
searchInput?.addEventListener('input', (e) => {
|
searchInput?.addEventListener('input', (e) => {
|
||||||
clearTimeout(searchTimeout);
|
clearTimeout(searchTimeout);
|
||||||
|
@ -488,18 +517,19 @@ const currentPath = Astro.url.pathname;
|
||||||
performSearch(e.target.value);
|
performSearch(e.target.value);
|
||||||
}, 300); // Debounce to avoid too many searches while typing
|
}, 300); // Debounce to avoid too many searches while typing
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle search form submission
|
// Handle search form submission (if your input is inside a form)
|
||||||
const searchForm = searchInput?.closest('form');
|
const searchForm = searchInput?.closest('form');
|
||||||
searchForm?.addEventListener('submit', (e) => {
|
searchForm?.addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault(); // Prevent default form submission
|
||||||
performSearch(searchInput.value);
|
performSearch(searchInput.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle search-submit button click
|
// Handle search-submit button click (if you have a separate submit button)
|
||||||
const searchSubmit = document.querySelector('.search-submit');
|
const searchSubmit = document.querySelector('.search-submit'); // Adjust selector if needed
|
||||||
searchSubmit?.addEventListener('click', () => {
|
searchSubmit?.addEventListener('click', () => {
|
||||||
performSearch(searchInput?.value || '');
|
performSearch(searchInput?.value || '');
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
}); // End of DOMContentLoaded
|
||||||
</script>
|
</script>
|
|
@ -8,6 +8,8 @@ import Footer from '../components/Footer.astro';
|
||||||
import Terminal from '../components/Terminal.astro';
|
import Terminal from '../components/Terminal.astro';
|
||||||
import KnowledgeGraph from '../components/KnowledgeGraph.astro';
|
import KnowledgeGraph from '../components/KnowledgeGraph.astro';
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
import { COMMON_COMMANDS, TERMINAL_CONTENT } from '../config/terminal.js';
|
||||||
|
|
||||||
// Get all blog entries
|
// Get all blog entries
|
||||||
const allPosts = await getCollection('posts');
|
const allPosts = await getCollection('posts');
|
||||||
|
@ -22,38 +24,8 @@ const sortedPosts = allPosts.sort((a, b) => {
|
||||||
// Get recent posts (latest 4)
|
// Get recent posts (latest 4)
|
||||||
const recentPosts = sortedPosts.slice(0, 4);
|
const recentPosts = sortedPosts.slice(0, 4);
|
||||||
|
|
||||||
// Prepare terminal commands
|
// Prepare terminal commands - now imported from central config
|
||||||
const terminalCommands = [
|
const terminalCommands = COMMON_COMMANDS;
|
||||||
{
|
|
||||||
prompt: "[laforceit@argobox]$ ",
|
|
||||||
command: "ls -la ./infrastructure",
|
|
||||||
output: [
|
|
||||||
"total 20",
|
|
||||||
"drwxr-xr-x 5 laforceit users 4096 Apr 23 09:15 <span class='highlight'>kubernetes/</span>",
|
|
||||||
"drwxr-xr-x 3 laforceit users 4096 Apr 20 17:22 <span class='highlight'>docker/</span>",
|
|
||||||
"drwxr-xr-x 2 laforceit users 4096 Apr 19 14:30 <span class='highlight'>networking/</span>",
|
|
||||||
"drwxr-xr-x 4 laforceit users 4096 Apr 22 21:10 <span class='highlight'>monitoring/</span>",
|
|
||||||
"drwxr-xr-x 3 laforceit users 4096 Apr 21 16:45 <span class='highlight'>storage/</span>",
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prompt: "[laforceit@argobox]$ ",
|
|
||||||
command: "find ./posts -type f -name \"*.md\" | wc -l",
|
|
||||||
output: [`${allPosts.length} posts found`]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
prompt: "[laforceit@argobox]$ ",
|
|
||||||
command: "kubectl get nodes",
|
|
||||||
output: [
|
|
||||||
"NAME STATUS ROLES AGE VERSION",
|
|
||||||
"argobox-cp1 Ready control-plane,master 92d v1.27.3",
|
|
||||||
"argobox-cp2 Ready control-plane,master 92d v1.27.3",
|
|
||||||
"argobox-cp3 Ready control-plane,master 92d v1.27.3",
|
|
||||||
"argobox-node1 Ready worker 92d v1.27.3",
|
|
||||||
"argobox-node2 Ready worker 92d v1.27.3"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
// Prepare graph data for knowledge map
|
// Prepare graph data for knowledge map
|
||||||
// Extract categories and tags from posts
|
// Extract categories and tags from posts
|
||||||
|
@ -364,6 +336,38 @@ const techStack = [
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Terminal Section -->
|
||||||
|
<section class="terminal-section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<Terminal
|
||||||
|
title="argobox:~/kubernetes"
|
||||||
|
promptPrefix={TERMINAL_DEFAULTS.promptPrefix}
|
||||||
|
height="400px"
|
||||||
|
command="kubectl get pods -A | head -8"
|
||||||
|
output={`NAMESPACE NAME READY STATUS RESTARTS AGE
|
||||||
|
kube-system coredns-66bff467f8-8p7z2 1/1 Running 0 15d
|
||||||
|
kube-system coredns-66bff467f8-v68vr 1/1 Running 0 15d
|
||||||
|
kube-system etcd-control-plane 1/1 Running 0 15d
|
||||||
|
kube-system kube-apiserver-control-plane 1/1 Running 0 15d
|
||||||
|
kube-system kube-controller-manager-control-plane 1/1 Running 0 15d
|
||||||
|
kube-system kube-proxy-c84qf 1/1 Running 0 15d
|
||||||
|
kube-system kube-scheduler-control-plane 1/1 Running 0 15d`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<Terminal
|
||||||
|
title="argobox:~/system"
|
||||||
|
promptPrefix={TERMINAL_DEFAULTS.promptPrefix}
|
||||||
|
height="400px"
|
||||||
|
content={SYSTEM_MONITOR_SEQUENCE.map(item => `<div class="term-blue">${item.prompt}</div><span>$</span> <span class="term-bold">${item.command}</span>\n${item.output.join('\n')}`).join('\n\n')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<Footer slot="footer" />
|
<Footer slot="footer" />
|
||||||
|
@ -993,6 +997,32 @@ const techStack = [
|
||||||
background: rgba(226, 232, 240, 0.2);
|
background: rgba(226, 232, 240, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Terminal Section */
|
||||||
|
.terminal-section {
|
||||||
|
padding: 6rem 0;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: clamp(1.75rem, 3vw, 2.5rem);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: 4px;
|
||||||
|
width: 60px;
|
||||||
|
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||||
|
bottom: -10px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.hero-content {
|
.hero-content {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -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>
|
|
@ -0,0 +1,341 @@
|
||||||
|
---
|
||||||
|
// MiniKnowledgeGraph.astro - Inline version that replaces the Tags section
|
||||||
|
// Designed to work within the existing sidebar structure
|
||||||
|
|
||||||
|
export interface GraphNode {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
type: 'post' | 'tag' | 'category';
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GraphEdge {
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
type: 'post-tag' | 'post-post';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currentPost: any;
|
||||||
|
relatedPosts?: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentPost, relatedPosts = [] } = Astro.props;
|
||||||
|
|
||||||
|
// Generate unique ID for the graph container
|
||||||
|
const graphId = `mini-cy-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
|
||||||
|
// Ensure currentPost has necessary properties
|
||||||
|
const safeCurrentPost = {
|
||||||
|
id: currentPost.slug || 'current-post',
|
||||||
|
title: currentPost.data?.title || 'Current Post',
|
||||||
|
tags: currentPost.data?.tags || [],
|
||||||
|
category: currentPost.data?.category || 'Uncategorized',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare graph data
|
||||||
|
const nodes: GraphNode[] = [];
|
||||||
|
const edges: GraphEdge[] = [];
|
||||||
|
const addedTagIds = new Set<string>();
|
||||||
|
const addedPostIds = new Set<string>();
|
||||||
|
|
||||||
|
// Add current post node
|
||||||
|
nodes.push({
|
||||||
|
id: safeCurrentPost.id,
|
||||||
|
label: safeCurrentPost.title,
|
||||||
|
type: 'post',
|
||||||
|
url: `/posts/${safeCurrentPost.id}/`
|
||||||
|
});
|
||||||
|
addedPostIds.add(safeCurrentPost.id);
|
||||||
|
|
||||||
|
// Add tags from current post
|
||||||
|
safeCurrentPost.tags.forEach((tag: string) => {
|
||||||
|
const tagId = `tag-${tag}`;
|
||||||
|
|
||||||
|
// Only add if not already added
|
||||||
|
if (!addedTagIds.has(tagId)) {
|
||||||
|
nodes.push({
|
||||||
|
id: tagId,
|
||||||
|
label: tag,
|
||||||
|
type: 'tag',
|
||||||
|
url: `/tag/${tag}/`
|
||||||
|
});
|
||||||
|
addedTagIds.add(tagId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add edge from current post to tag
|
||||||
|
edges.push({
|
||||||
|
source: safeCurrentPost.id,
|
||||||
|
target: tagId,
|
||||||
|
type: 'post-tag'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add related posts and their connections
|
||||||
|
if (relatedPosts && relatedPosts.length > 0) {
|
||||||
|
relatedPosts.forEach(post => {
|
||||||
|
if (!post) return;
|
||||||
|
|
||||||
|
const postId = post.slug || `post-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
|
||||||
|
// Skip if already added or is the current post
|
||||||
|
if (addedPostIds.has(postId) || postId === safeCurrentPost.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add related post node
|
||||||
|
nodes.push({
|
||||||
|
id: postId,
|
||||||
|
label: post.data?.title || 'Related Post',
|
||||||
|
type: 'post',
|
||||||
|
url: `/posts/${postId}/`
|
||||||
|
});
|
||||||
|
addedPostIds.add(postId);
|
||||||
|
|
||||||
|
// Add edge from current post to related post
|
||||||
|
edges.push({
|
||||||
|
source: safeCurrentPost.id,
|
||||||
|
target: postId,
|
||||||
|
type: 'post-post'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add shared tags and their connections
|
||||||
|
const postTags = post.data?.tags || [];
|
||||||
|
postTags.forEach((tag: string) => {
|
||||||
|
// Only add connections for tags that the current post also has
|
||||||
|
if (safeCurrentPost.tags.includes(tag)) {
|
||||||
|
const tagId = `tag-${tag}`;
|
||||||
|
|
||||||
|
// Add edge from related post to shared tag
|
||||||
|
edges.push({
|
||||||
|
source: postId,
|
||||||
|
target: tagId,
|
||||||
|
type: 'post-tag'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate graph data
|
||||||
|
const graphData = { nodes, edges };
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="sidebar-card knowledge-graph-card">
|
||||||
|
<h3 class="sidebar-title">Post Connections</h3>
|
||||||
|
<div class="mini-knowledge-graph">
|
||||||
|
<div id={graphId} class="mini-cy"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.knowledge-graph-card {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-knowledge-graph {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-cy {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
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>
|
||||||
|
|
||||||
|
<script define:vars={{ graphId, graphData }}>
|
||||||
|
// Initialize the miniature knowledge graph
|
||||||
|
function initializeMiniGraph() {
|
||||||
|
// Ensure Cytoscape is available
|
||||||
|
if (typeof cytoscape === 'undefined') {
|
||||||
|
console.error('[MiniKnowledgeGraph] Cytoscape library not loaded.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the container
|
||||||
|
const container = document.getElementById(graphId);
|
||||||
|
if (!container) {
|
||||||
|
console.error(`[MiniKnowledgeGraph] Container #${graphId} not found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if we have any nodes to display
|
||||||
|
if (!graphData.nodes || graphData.nodes.length === 0) {
|
||||||
|
console.warn('[MiniKnowledgeGraph] No nodes to display.');
|
||||||
|
container.innerHTML = '<div style="display:flex;height:100%;align-items:center;justify-content:center;color:var(--text-secondary);">No connections available</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize Cytoscape with improved layout parameters for small space
|
||||||
|
const cy = cytoscape({
|
||||||
|
container,
|
||||||
|
elements: [
|
||||||
|
...graphData.nodes.map(node => ({
|
||||||
|
data: {
|
||||||
|
id: node.id,
|
||||||
|
label: node.label,
|
||||||
|
type: node.type,
|
||||||
|
url: node.url
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
...graphData.edges.map((edge, index) => ({
|
||||||
|
data: {
|
||||||
|
id: `e${index}`,
|
||||||
|
source: edge.source,
|
||||||
|
target: edge.target,
|
||||||
|
type: edge.type
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
style: [
|
||||||
|
// Node styling
|
||||||
|
{
|
||||||
|
selector: 'node',
|
||||||
|
style: {
|
||||||
|
'background-color': '#3B82F6', // Default blue for posts
|
||||||
|
'label': 'data(label)',
|
||||||
|
'width': 15,
|
||||||
|
'height': 15,
|
||||||
|
'font-size': '8px',
|
||||||
|
'color': '#E2E8F0',
|
||||||
|
'text-valign': 'bottom',
|
||||||
|
'text-halign': 'center',
|
||||||
|
'text-margin-y': 4,
|
||||||
|
'text-wrap': 'ellipsis',
|
||||||
|
'text-max-width': '60px',
|
||||||
|
'border-width': 1,
|
||||||
|
'border-color': '#0F1219',
|
||||||
|
'border-opacity': 0.8
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Post node specific styles
|
||||||
|
{
|
||||||
|
selector: 'node[type="post"]',
|
||||||
|
style: {
|
||||||
|
'background-color': '#3B82F6', // Blue for posts
|
||||||
|
'shape': 'ellipse',
|
||||||
|
'width': 18,
|
||||||
|
'height': 18
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Current post node (first in the nodes array)
|
||||||
|
{
|
||||||
|
selector: `#${graphData.nodes[0]?.id}`,
|
||||||
|
style: {
|
||||||
|
'background-color': '#06B6D4', // Cyan for current post
|
||||||
|
'width': 25,
|
||||||
|
'height': 25,
|
||||||
|
'border-width': 2,
|
||||||
|
'border-color': '#E2E8F0'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Tag node specific styles
|
||||||
|
{
|
||||||
|
selector: 'node[type="tag"]',
|
||||||
|
style: {
|
||||||
|
'background-color': '#10B981', // Green for tags
|
||||||
|
'shape': 'diamond',
|
||||||
|
'width': 15,
|
||||||
|
'height': 15
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Edge styles
|
||||||
|
{
|
||||||
|
selector: 'edge',
|
||||||
|
style: {
|
||||||
|
'width': 1,
|
||||||
|
'line-color': 'rgba(226, 232, 240, 0.4)',
|
||||||
|
'curve-style': 'bezier',
|
||||||
|
'opacity': 0.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Post-tag edge specific styles
|
||||||
|
{
|
||||||
|
selector: 'edge[type="post-tag"]',
|
||||||
|
style: {
|
||||||
|
'line-color': 'rgba(16, 185, 129, 0.6)', // Green
|
||||||
|
'line-style': 'dashed'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Post-post edge specific styles
|
||||||
|
{
|
||||||
|
selector: 'edge[type="post-post"]',
|
||||||
|
style: {
|
||||||
|
'line-color': 'rgba(59, 130, 246, 0.6)', // Blue
|
||||||
|
'line-style': 'solid',
|
||||||
|
'width': 1.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Hover styles
|
||||||
|
{
|
||||||
|
selector: 'node:hover',
|
||||||
|
style: {
|
||||||
|
'background-color': '#F59E0B', // Amber on hover
|
||||||
|
'border-color': '#FFFFFF',
|
||||||
|
'border-width': 2,
|
||||||
|
'cursor': 'pointer'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// Use a compact layout for sidebar
|
||||||
|
layout: {
|
||||||
|
name: 'cose',
|
||||||
|
animate: false,
|
||||||
|
fit: true,
|
||||||
|
padding: 5,
|
||||||
|
nodeRepulsion: function(node) {
|
||||||
|
return 10000; // Stronger repulsion to prevent overlap in small space
|
||||||
|
},
|
||||||
|
idealEdgeLength: 50,
|
||||||
|
edgeElasticity: 0.45,
|
||||||
|
nestingFactor: 0.1,
|
||||||
|
gravity: 0.25,
|
||||||
|
numIter: 1500,
|
||||||
|
initialTemp: 1000,
|
||||||
|
coolingFactor: 0.99,
|
||||||
|
minTemp: 1.0
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add click event for nodes
|
||||||
|
cy.on('tap', 'node', function(evt) {
|
||||||
|
const node = evt.target;
|
||||||
|
const url = node.data('url');
|
||||||
|
if (url) {
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Center the graph
|
||||||
|
cy.fit(undefined, 10);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MiniKnowledgeGraph] Error initializing Cytoscape:', error);
|
||||||
|
container.innerHTML = '<div style="padding:10px;color:var(--text-secondary);">Error loading graph</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for DOM to be ready and ensure proper initialization
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Delay initialization slightly to ensure container has dimensions
|
||||||
|
setTimeout(initializeMiniGraph, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also handle the case where the script loads after DOMContentLoaded
|
||||||
|
if (document.readyState === 'complete' || document.readyState === 'interactive') {
|
||||||
|
setTimeout(initializeMiniGraph, 100);
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -2,37 +2,32 @@
|
||||||
// Terminal.astro
|
// Terminal.astro
|
||||||
// A component that displays terminal-like interface with animated commands and outputs
|
// A component that displays terminal-like interface with animated commands and outputs
|
||||||
|
|
||||||
interface Command {
|
export interface Props {
|
||||||
prompt: string;
|
|
||||||
command: string;
|
|
||||||
output?: string[];
|
|
||||||
delay?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
commands: Command[];
|
|
||||||
title?: string;
|
title?: string;
|
||||||
theme?: 'dark' | 'light';
|
height?: string;
|
||||||
interactive?: boolean;
|
|
||||||
showTitleBar?: boolean;
|
showTitleBar?: boolean;
|
||||||
|
showPrompt?: boolean;
|
||||||
|
commands?: {
|
||||||
|
prompt: string;
|
||||||
|
command: string;
|
||||||
|
output?: string[];
|
||||||
|
delay?: number;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
commands,
|
title = "terminal",
|
||||||
title = "argobox:~/homelab",
|
height = "auto",
|
||||||
theme = "dark",
|
showTitleBar = true,
|
||||||
interactive = false,
|
showPrompt = true,
|
||||||
showTitleBar = true
|
commands = []
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
|
|
||||||
// Make the last command have the typing effect
|
// Make the last command have the typing effect
|
||||||
const lastIndex = commands.length - 1;
|
const lastIndex = commands.length - 1;
|
||||||
|
|
||||||
// Conditionally add classes based on props
|
|
||||||
const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : ''}`;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class={terminalClasses}>
|
<div class="terminal-box">
|
||||||
{showTitleBar && (
|
{showTitleBar && (
|
||||||
<div class="terminal-header">
|
<div class="terminal-header">
|
||||||
<div class="terminal-dots">
|
<div class="terminal-dots">
|
||||||
|
@ -59,7 +54,7 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div class="terminal-content">
|
<div class="terminal-content" style={`height: ${height};`}>
|
||||||
{commands.map((cmd, index) => (
|
{commands.map((cmd, index) => (
|
||||||
<div class="terminal-block">
|
<div class="terminal-block">
|
||||||
<div class="terminal-line">
|
<div class="terminal-line">
|
||||||
|
@ -78,15 +73,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{interactive && (
|
|
||||||
<div class="terminal-block terminal-interactive">
|
|
||||||
<div class="terminal-line">
|
|
||||||
<span class="terminal-prompt">guest@argobox:~$</span>
|
|
||||||
<input type="text" class="terminal-input" placeholder="Type 'help' for available commands" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="terminal-cursor"></div>
|
<div class="terminal-cursor"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -107,34 +93,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme */
|
|
||||||
.terminal-light {
|
|
||||||
background: #f0f4f8;
|
|
||||||
border-color: #d1dce5;
|
|
||||||
box-shadow: 0 0 30px rgba(0, 0, 0, 0.07);
|
|
||||||
color: #1a202c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-light .terminal-prompt {
|
|
||||||
color: #2a7ac0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-light .terminal-command {
|
|
||||||
color: #1a202c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-light .terminal-output {
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-light .terminal-header {
|
|
||||||
border-bottom: 1px solid #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-light .terminal-title {
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
/* Header */
|
||||||
.terminal-header {
|
.terminal-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -261,22 +219,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Interactive elements */
|
|
||||||
.terminal-interactive {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.terminal-input {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
color: var(--text-primary, #e2e8f0);
|
|
||||||
font-family: var(--font-mono, 'JetBrains Mono', monospace);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
flex: 1;
|
|
||||||
outline: none;
|
|
||||||
caret-color: transparent; /* Hide default cursor */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blinking cursor */
|
/* Blinking cursor */
|
||||||
.terminal-cursor {
|
.terminal-cursor {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -290,10 +232,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-interactive:has(.terminal-input:focus) ~ .terminal-cursor {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Typing effect */
|
/* Typing effect */
|
||||||
.terminal-typing {
|
.terminal-typing {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -335,13 +273,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
||||||
.terminal-box:hover .terminal-dot-green {
|
.terminal-box:hover .terminal-dot-green {
|
||||||
background: #34d399;
|
background: #34d399;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.terminal-box {
|
|
||||||
height: 300px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -383,13 +314,15 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
||||||
const cursor = typingElement.closest('.terminal-box').querySelector('.terminal-cursor');
|
const cursor = typingElement.closest('.terminal-box').querySelector('.terminal-cursor');
|
||||||
if (cursor) {
|
if (cursor) {
|
||||||
const rect = typingElement.getBoundingClientRect();
|
const rect = typingElement.getBoundingClientRect();
|
||||||
const parentRect = typingElement.closest('.terminal-content').getBoundingClientRect();
|
if (terminalContent) {
|
||||||
|
const parentRect = terminalContent.getBoundingClientRect();
|
||||||
// Position cursor after the last character
|
|
||||||
cursor.style.opacity = '1';
|
// Position cursor after the last character
|
||||||
cursor.style.left = `${rect.left - parentRect.left + typingElement.offsetWidth}px`;
|
cursor.style.opacity = '1';
|
||||||
cursor.style.top = `${rect.top - parentRect.top}px`;
|
cursor.style.left = `${rect.left - parentRect.left + typingElement.offsetWidth}px`;
|
||||||
cursor.style.height = `${rect.height}px`;
|
cursor.style.top = `${rect.top - parentRect.top}px`;
|
||||||
|
cursor.style.height = `${rect.height}px`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -399,210 +332,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
||||||
}, 1000 * elementIndex); // Sequential delay for multiple typing elements
|
}, 1000 * elementIndex); // Sequential delay for multiple typing elements
|
||||||
});
|
});
|
||||||
|
|
||||||
// Interactive terminal functionality
|
|
||||||
const interactiveTerminals = document.querySelectorAll('.terminal-interactive');
|
|
||||||
|
|
||||||
interactiveTerminals.forEach(terminal => {
|
|
||||||
const input = terminal.querySelector('.terminal-input');
|
|
||||||
const terminalContent = terminal.closest('.terminal-content');
|
|
||||||
const prompt = terminal.querySelector('.terminal-prompt').textContent;
|
|
||||||
|
|
||||||
if (!input || !terminalContent) return;
|
|
||||||
|
|
||||||
// Position cursor when input is focused
|
|
||||||
input.addEventListener('focus', () => {
|
|
||||||
const cursor = terminal.closest('.terminal-box').querySelector('.terminal-cursor');
|
|
||||||
if (cursor) {
|
|
||||||
const rect = input.getBoundingClientRect();
|
|
||||||
const parentRect = terminalContent.getBoundingClientRect();
|
|
||||||
|
|
||||||
cursor.style.left = `${rect.left - parentRect.left + input.value.length * 8}px`;
|
|
||||||
cursor.style.top = `${rect.top - parentRect.top}px`;
|
|
||||||
cursor.style.height = `${rect.height}px`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update cursor position as user types
|
|
||||||
input.addEventListener('input', () => {
|
|
||||||
const cursor = terminal.closest('.terminal-box').querySelector('.terminal-cursor');
|
|
||||||
if (cursor) {
|
|
||||||
const rect = input.getBoundingClientRect();
|
|
||||||
const parentRect = terminalContent.getBoundingClientRect();
|
|
||||||
|
|
||||||
cursor.style.left = `${rect.left - parentRect.left + 8 * (prompt.length + input.value.length) + 8}px`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process command on Enter
|
|
||||||
input.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const command = input.value.trim();
|
|
||||||
if (!command) return;
|
|
||||||
|
|
||||||
// Create new command block
|
|
||||||
const commandBlock = document.createElement('div');
|
|
||||||
commandBlock.className = 'terminal-block';
|
|
||||||
|
|
||||||
const commandLine = document.createElement('div');
|
|
||||||
commandLine.className = 'terminal-line';
|
|
||||||
|
|
||||||
const promptSpan = document.createElement('span');
|
|
||||||
promptSpan.className = 'terminal-prompt';
|
|
||||||
promptSpan.textContent = prompt;
|
|
||||||
|
|
||||||
const commandSpan = document.createElement('span');
|
|
||||||
commandSpan.className = 'terminal-command';
|
|
||||||
commandSpan.textContent = command;
|
|
||||||
|
|
||||||
commandLine.appendChild(promptSpan);
|
|
||||||
commandLine.appendChild(commandSpan);
|
|
||||||
commandBlock.appendChild(commandLine);
|
|
||||||
|
|
||||||
// Process command and add output
|
|
||||||
const output = processCommand(command);
|
|
||||||
|
|
||||||
if (output && output.length > 0) {
|
|
||||||
const outputDiv = document.createElement('div');
|
|
||||||
outputDiv.className = 'terminal-output';
|
|
||||||
|
|
||||||
output.forEach(line => {
|
|
||||||
const lineDiv = document.createElement('div');
|
|
||||||
lineDiv.className = 'terminal-output-line';
|
|
||||||
lineDiv.innerHTML = line;
|
|
||||||
outputDiv.appendChild(lineDiv);
|
|
||||||
});
|
|
||||||
|
|
||||||
commandBlock.appendChild(outputDiv);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert before the interactive block
|
|
||||||
terminal.parentNode.insertBefore(commandBlock, terminal);
|
|
||||||
|
|
||||||
// Clear input
|
|
||||||
input.value = '';
|
|
||||||
|
|
||||||
// Scroll to bottom
|
|
||||||
terminalContent.scrollTop = terminalContent.scrollHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Define available commands and their outputs
|
|
||||||
function processCommand(cmd) {
|
|
||||||
const commands = {
|
|
||||||
'help': [
|
|
||||||
'<span class="highlight">Available commands:</span>',
|
|
||||||
' help - Display this help message',
|
|
||||||
' clear - Clear the terminal',
|
|
||||||
' ls - List available resources',
|
|
||||||
' cat [file] - View file contents',
|
|
||||||
' about - About this site',
|
|
||||||
' status - Check system status',
|
|
||||||
' uname -a - Display system information'
|
|
||||||
],
|
|
||||||
'clear': [],
|
|
||||||
'about': [
|
|
||||||
'<span class="highlight">LaForceIT</span>',
|
|
||||||
'A tech blog focused on home lab infrastructure, Kubernetes,',
|
|
||||||
'Docker, and DevOps best practices.',
|
|
||||||
'',
|
|
||||||
'Created by Daniel LaForce',
|
|
||||||
'Type <span class="highlight">\'help\'</span> to see available commands'
|
|
||||||
],
|
|
||||||
'uname -a': [
|
|
||||||
'ArgoBox-Lite 5.15.0-69-generic #76-Ubuntu SMP Fri Mar 17 17:19:29 UTC 2023 x86_64',
|
|
||||||
'Hardware: ProxmoxVE 8.0.4 | Intel(R) Core(TM) i7-12700K | 64GB RAM'
|
|
||||||
],
|
|
||||||
'status': [
|
|
||||||
'<span class="highlight">System Status:</span>',
|
|
||||||
'<span class="success">✓</span> ArgoBox: Online',
|
|
||||||
'<span class="success">✓</span> Kubernetes: Running',
|
|
||||||
'<span class="success">✓</span> Docker Registry: Active',
|
|
||||||
'<span class="success">✓</span> Gitea: Online',
|
|
||||||
'<span class="warning">⚠</span> Monitoring: Degraded - Check Grafana instance'
|
|
||||||
],
|
|
||||||
'ls': [
|
|
||||||
'<span class="highlight">Available resources:</span>',
|
|
||||||
'kubernetes/ docker/ networking/',
|
|
||||||
'homelab.md configs.yaml setup-guide.md',
|
|
||||||
'resources.json projects.md'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check for cat command
|
|
||||||
if (cmd.startsWith('cat ')) {
|
|
||||||
const file = cmd.split(' ')[1];
|
|
||||||
|
|
||||||
const fileContents = {
|
|
||||||
'homelab.md': [
|
|
||||||
'<span class="highlight">## HomeLab Setup Guide</span>',
|
|
||||||
'This document outlines my personal home lab setup,',
|
|
||||||
'including hardware specifications, network configuration,',
|
|
||||||
'and installed services.',
|
|
||||||
'',
|
|
||||||
'See the full guide at: /homelab'
|
|
||||||
],
|
|
||||||
'configs.yaml': [
|
|
||||||
'apiVersion: v1',
|
|
||||||
'kind: ConfigMap',
|
|
||||||
'metadata:',
|
|
||||||
' name: argobox-config',
|
|
||||||
' namespace: default',
|
|
||||||
'data:',
|
|
||||||
' POSTGRES_HOST: "db.local"',
|
|
||||||
' REDIS_HOST: "cache.local"',
|
|
||||||
' ...'
|
|
||||||
],
|
|
||||||
'setup-guide.md': [
|
|
||||||
'<span class="highlight">## Quick Start Guide</span>',
|
|
||||||
'1. Install Proxmox on bare metal hardware',
|
|
||||||
'2. Deploy K3s cluster using Ansible playbooks',
|
|
||||||
'3. Configure storage using Longhorn',
|
|
||||||
'4. Deploy ArgoCD for GitOps workflow',
|
|
||||||
'...'
|
|
||||||
],
|
|
||||||
'resources.json': [
|
|
||||||
'{',
|
|
||||||
' "cpu": "12 cores",',
|
|
||||||
' "memory": "64GB",',
|
|
||||||
' "storage": "8TB",',
|
|
||||||
' "network": "10Gbit"',
|
|
||||||
'}'
|
|
||||||
],
|
|
||||||
'projects.md': [
|
|
||||||
'<span class="highlight">## Current Projects</span>',
|
|
||||||
'- <span class="success">ArgoBox</span>: Self-hosted deployment platform',
|
|
||||||
'- <span class="success">K8s Monitor</span>: Custom Kubernetes dashboard',
|
|
||||||
'- <span class="warning">Media Server</span>: In progress',
|
|
||||||
'- <span class="highlight">See all projects at:</span> /projects'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
if (fileContents[file]) {
|
|
||||||
return fileContents[file];
|
|
||||||
} else {
|
|
||||||
return [`<span class="error">Error: File '${file}' not found.</span>`];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle unknown commands
|
|
||||||
if (!commands[cmd]) {
|
|
||||||
return [`<span class="error">Command not found: ${cmd}</span>`, 'Type <span class="highlight">\'help\'</span> to see available commands'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle clear command
|
|
||||||
if (cmd === 'clear') {
|
|
||||||
// Remove all blocks except the interactive one
|
|
||||||
const blocks = terminalContent.querySelectorAll('.terminal-block:not(.terminal-interactive)');
|
|
||||||
blocks.forEach(block => block.remove());
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return commands[cmd];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Button interactions
|
// Button interactions
|
||||||
const minButtons = document.querySelectorAll('.terminal-button-minimize');
|
const minButtons = document.querySelectorAll('.terminal-button-minimize');
|
||||||
const maxButtons = document.querySelectorAll('.terminal-button-maximize');
|
const maxButtons = document.querySelectorAll('.terminal-button-maximize');
|
||||||
|
@ -610,17 +339,23 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
||||||
minButtons.forEach(button => {
|
minButtons.forEach(button => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
const terminalBox = button.closest('.terminal-box');
|
const terminalBox = button.closest('.terminal-box');
|
||||||
terminalBox.classList.toggle('minimized');
|
if (terminalBox) {
|
||||||
|
terminalBox.classList.toggle('minimized');
|
||||||
if (terminalBox.classList.contains('minimized')) {
|
|
||||||
const content = terminalBox.querySelector('.terminal-content');
|
if (terminalBox.classList.contains('minimized')) {
|
||||||
terminalBox.dataset.prevHeight = terminalBox.style.height;
|
const content = terminalBox.querySelector('.terminal-content');
|
||||||
terminalBox.style.height = '40px';
|
if (content) {
|
||||||
content.style.display = 'none';
|
terminalBox.dataset.prevHeight = terminalBox.style.height;
|
||||||
} else {
|
terminalBox.style.height = '40px';
|
||||||
const content = terminalBox.querySelector('.terminal-content');
|
content.style.display = 'none';
|
||||||
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
|
}
|
||||||
content.style.display = 'block';
|
} else {
|
||||||
|
const content = terminalBox.querySelector('.terminal-content');
|
||||||
|
if (content) {
|
||||||
|
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
|
||||||
|
content.style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -628,30 +363,31 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
||||||
maxButtons.forEach(button => {
|
maxButtons.forEach(button => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
const terminalBox = button.closest('.terminal-box');
|
const terminalBox = button.closest('.terminal-box');
|
||||||
terminalBox.classList.toggle('maximized');
|
if (terminalBox) {
|
||||||
|
terminalBox.classList.toggle('maximized');
|
||||||
if (terminalBox.classList.contains('maximized')) {
|
|
||||||
const content = terminalBox.querySelector('.terminal-content');
|
|
||||||
terminalBox.dataset.prevHeight = terminalBox.style.height;
|
|
||||||
terminalBox.dataset.prevWidth = terminalBox.style.width;
|
|
||||||
terminalBox.dataset.prevPosition = terminalBox.style.position;
|
|
||||||
|
|
||||||
terminalBox.style.position = 'fixed';
|
if (terminalBox.classList.contains('maximized')) {
|
||||||
terminalBox.style.top = '0';
|
const content = terminalBox.querySelector('.terminal-content');
|
||||||
terminalBox.style.left = '0';
|
terminalBox.dataset.prevHeight = terminalBox.style.height;
|
||||||
terminalBox.style.width = '100%';
|
terminalBox.dataset.prevWidth = terminalBox.style.width;
|
||||||
terminalBox.style.height = '100%';
|
terminalBox.dataset.prevPosition = terminalBox.style.position;
|
||||||
terminalBox.style.zIndex = '9999';
|
|
||||||
terminalBox.style.borderRadius = '0';
|
terminalBox.style.position = 'fixed';
|
||||||
} else {
|
terminalBox.style.top = '0';
|
||||||
const content = terminalBox.querySelector('.terminal-content');
|
terminalBox.style.left = '0';
|
||||||
terminalBox.style.position = terminalBox.dataset.prevPosition || 'relative';
|
terminalBox.style.width = '100%';
|
||||||
terminalBox.style.width = terminalBox.dataset.prevWidth || '100%';
|
terminalBox.style.height = '100%';
|
||||||
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
|
terminalBox.style.zIndex = '9999';
|
||||||
terminalBox.style.zIndex = 'auto';
|
terminalBox.style.borderRadius = '0';
|
||||||
terminalBox.style.borderRadius = '10px';
|
} else {
|
||||||
terminalBox.style.top = 'auto';
|
terminalBox.style.position = terminalBox.dataset.prevPosition || 'relative';
|
||||||
terminalBox.style.left = 'auto';
|
terminalBox.style.width = terminalBox.dataset.prevWidth || '100%';
|
||||||
|
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
|
||||||
|
terminalBox.style.zIndex = 'auto';
|
||||||
|
terminalBox.style.borderRadius = '10px';
|
||||||
|
terminalBox.style.top = 'auto';
|
||||||
|
terminalBox.style.left = 'auto';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,414 @@
|
||||||
|
/**
|
||||||
|
* Terminal Configuration
|
||||||
|
* Central configuration for the Terminal component across the site
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Default terminal prompt settings
|
||||||
|
export const TERMINAL_DEFAULTS = {
|
||||||
|
promptPrefix: "[laforceit@argobox]",
|
||||||
|
title: "argobox:~/blog",
|
||||||
|
theme: "dark", // Default theme (dark or light)
|
||||||
|
height: "auto",
|
||||||
|
showTitleBar: true,
|
||||||
|
showPrompt: true
|
||||||
|
};
|
||||||
|
|
||||||
|
// Commonly used commands
|
||||||
|
export const COMMON_COMMANDS = [
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "ls -la ./infrastructure",
|
||||||
|
output: [
|
||||||
|
"total 20",
|
||||||
|
"drwxr-xr-x 5 laforceit users 4096 Apr 23 09:15 <span class='highlight'>kubernetes/</span>",
|
||||||
|
"drwxr-xr-x 3 laforceit users 4096 Apr 20 17:22 <span class='highlight'>docker/</span>",
|
||||||
|
"drwxr-xr-x 2 laforceit users 4096 Apr 19 14:30 <span class='highlight'>networking/</span>",
|
||||||
|
"drwxr-xr-x 4 laforceit users 4096 Apr 22 21:10 <span class='highlight'>monitoring/</span>",
|
||||||
|
"drwxr-xr-x 3 laforceit users 4096 Apr 21 16:45 <span class='highlight'>storage/</span>",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "grep -r \"kubernetes\" --include=\"*.md\" ./posts | wc -l",
|
||||||
|
output: ["7 matches found"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "kubectl get nodes",
|
||||||
|
output: [
|
||||||
|
"NAME STATUS ROLES AGE VERSION",
|
||||||
|
"argobox-cp1 Ready control-plane,master 92d v1.27.3",
|
||||||
|
"argobox-cp2 Ready control-plane,master 92d v1.27.3",
|
||||||
|
"argobox-cp3 Ready control-plane,master 92d v1.27.3",
|
||||||
|
"argobox-node1 Ready worker 92d v1.27.3",
|
||||||
|
"argobox-node2 Ready worker 92d v1.27.3"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Advanced blog search command sequence
|
||||||
|
export const BLOG_SEARCH_SEQUENCE = [
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "cd ./posts && grep -r \"homelab\" --include=\"*.md\" | sort | head -5",
|
||||||
|
output: [
|
||||||
|
"<span class='term-green'>homelab-essentials.md</span>:<span class='term-blue'>title:</span> \"Essential Tools for Your Home Lab Setup\"",
|
||||||
|
"<span class='term-green'>homelab-essentials.md</span>:<span class='term-blue'>description:</span> \"A curated list of must-have tools for building your home lab infrastructure\"",
|
||||||
|
"<span class='term-green'>kubernetes-at-home.md</span>:<span class='term-blue'>title:</span> \"Running Kubernetes in Your Homelab\"",
|
||||||
|
"<span class='term-green'>proxmox-cluster.md</span>:<span class='term-blue'>description:</span> \"Building a resilient homelab foundation with Proxmox VE cluster\"",
|
||||||
|
"<span class='term-green'>storage-solutions.md</span>:<span class='term-blue'>body:</span> \"...affordable homelab storage solutions for a growing collection of VMs and containers...\""
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "find ./posts -type f -name \"*.md\" | xargs wc -l | sort -nr | head -3",
|
||||||
|
output: [
|
||||||
|
"2567 total",
|
||||||
|
" 842 ./posts/kubernetes-the-hard-way.md",
|
||||||
|
" 756 ./posts/home-automation-guide.md",
|
||||||
|
" 523 ./posts/proxmox-cluster.md"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// System monitoring sequence
|
||||||
|
export const SYSTEM_MONITOR_SEQUENCE = [
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "htop",
|
||||||
|
output: [
|
||||||
|
"<span class='term-purple'>Tasks:</span> <span class='term-cyan'>143</span> total, <span class='term-green'>4</span> running, <span class='term-yellow'>139</span> sleeping, <span class='term-red'>0</span> stopped, <span class='term-red'>0</span> zombie",
|
||||||
|
"<span class='term-purple'>%Cpu(s):</span> <span class='term-green'>12.5</span> us, <span class='term-blue'>4.2</span> sy, <span class='term-cyan'>0.0</span> ni, <span class='term-green'>82.3</span> id, <span class='term-yellow'>0.7</span> wa, <span class='term-red'>0.0</span> hi, <span class='term-red'>0.3</span> si, <span class='term-cyan'>0.0</span> st",
|
||||||
|
"<span class='term-purple'>MiB Mem:</span> <span class='term-cyan'>32102.3</span> total, <span class='term-green'>12023.4</span> free, <span class='term-yellow'>10654.8</span> used, <span class='term-blue'>9424.1</span> buff/cache",
|
||||||
|
"<span class='term-purple'>MiB Swap:</span> <span class='term-cyan'>16384.0</span> total, <span class='term-green'>16384.0</span> free, <span class='term-yellow'>0.0</span> used. <span class='term-green'>20223.3</span> avail Mem",
|
||||||
|
"",
|
||||||
|
" <span class='term-cyan'>PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND</span>",
|
||||||
|
"<span class='term-yellow'> 23741 laforcei 20 0 4926.0m 257.9m 142.1m S 25.0 0.8 42:36.76 node</span>",
|
||||||
|
" 22184 root 20 0 743.9m 27.7m 17.6m S 6.2 0.1 27:57.21 dockerd",
|
||||||
|
" 15532 root 20 0 1735.9m 203.5m 122.1m S 6.2 0.6 124:29.93 k3s-server",
|
||||||
|
" 1126 prometheu 20 0 1351.5m 113.9m 41.3m S 0.0 0.4 3:12.52 prometheus"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "df -h",
|
||||||
|
output: [
|
||||||
|
"Filesystem Size Used Avail Use% Mounted on",
|
||||||
|
"/dev/nvme0n1p2 932G 423G 462G 48% /",
|
||||||
|
"/dev/nvme1n1 1.8T 1.1T 638G 64% /data",
|
||||||
|
"tmpfs 16G 12M 16G 1% /run",
|
||||||
|
"tmpfs 32G 0 32G 0% /dev/shm"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "docker stats --no-stream",
|
||||||
|
output: [
|
||||||
|
"CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS",
|
||||||
|
"7d9915b1f946 blog-site 0.15% 145.6MiB / 32GiB 0.44% 648kB / 4.21MB 12.3MB / 0B 24",
|
||||||
|
"c7823beac704 prometheus 2.33% 175.2MiB / 32GiB 0.53% 15.5MB / 25.4MB 29.6MB / 12.4MB 15",
|
||||||
|
"db9d8512f471 postgres 0.03% 96.45MiB / 32GiB 0.29% 85.1kB / 106kB 21.9MB / 63.5MB 11",
|
||||||
|
"f3b1c9e2a147 grafana 0.42% 78.32MiB / 32GiB 0.24% 5.42MB / 12.7MB 86.4MB / 1.21MB 13"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Blog deployment sequence
|
||||||
|
export const BLOG_DEPLOYMENT_SEQUENCE = [
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "git status",
|
||||||
|
output: [
|
||||||
|
"On branch <span class='term-cyan'>main</span>",
|
||||||
|
"Your branch is up to date with 'origin/main'.",
|
||||||
|
"",
|
||||||
|
"Changes not staged for commit:",
|
||||||
|
" (use \"git add <file>...\" to update what will be committed)",
|
||||||
|
" (use \"git restore <file>...\" to discard changes in working directory)",
|
||||||
|
" <span class='term-red'>modified: src/content/posts/kubernetes-at-home.md</span>",
|
||||||
|
" <span class='term-red'>modified: src/components/Terminal.astro</span>",
|
||||||
|
"",
|
||||||
|
"Untracked files:",
|
||||||
|
" (use \"git add <file>...\" to include in what will be committed)",
|
||||||
|
" <span class='term-red'>src/content/posts/new-homelab-upgrades.md</span>",
|
||||||
|
"",
|
||||||
|
"no changes added to commit (use \"git add\" and/or \"git commit -a\")"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "git add . && git commit -m \"feat: add new post about homelab upgrades\"",
|
||||||
|
output: [
|
||||||
|
"[main <span class='term-green'>f92d47a</span>] <span class='term-cyan'>feat: add new post about homelab upgrades</span>",
|
||||||
|
" 3 files changed, 214 insertions(+), 12 deletions(-)",
|
||||||
|
" create mode 100644 src/content/posts/new-homelab-upgrades.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "npm run build && npm run deploy",
|
||||||
|
output: [
|
||||||
|
"<span class='term-green'>✓</span> Building for production...",
|
||||||
|
"<span class='term-green'>✓</span> Generating static routes",
|
||||||
|
"<span class='term-green'>✓</span> Client side rendering with hydration",
|
||||||
|
"<span class='term-green'>✓</span> Applying optimizations",
|
||||||
|
"<span class='term-green'>✓</span> Complete! 187 pages generated in 43.2 seconds",
|
||||||
|
"",
|
||||||
|
"<span class='term-blue'>Deploying to production environment...</span>",
|
||||||
|
"<span class='term-green'>✓</span> Upload complete",
|
||||||
|
"<span class='term-green'>✓</span> CDN cache invalidated",
|
||||||
|
"<span class='term-green'>✓</span> DNS configuration verified",
|
||||||
|
"<span class='term-green'>✓</span> Blog is live at https://laforceit.com!"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Kubernetes operation sequence
|
||||||
|
export const K8S_OPERATION_SEQUENCE = [
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "kubectl create namespace blog-prod",
|
||||||
|
output: [
|
||||||
|
"namespace/blog-prod created"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "kubectl apply -f kubernetes/blog-deployment.yaml",
|
||||||
|
output: [
|
||||||
|
"deployment.apps/blog-frontend created",
|
||||||
|
"service/blog-frontend created",
|
||||||
|
"configmap/blog-config created",
|
||||||
|
"secret/blog-secrets created"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "kubectl get pods -n blog-prod",
|
||||||
|
output: [
|
||||||
|
"NAME READY STATUS RESTARTS AGE",
|
||||||
|
"blog-frontend-7d9b5c7b8d-2xprm 1/1 Running 0 35s",
|
||||||
|
"blog-frontend-7d9b5c7b8d-8bkpl 1/1 Running 0 35s",
|
||||||
|
"blog-frontend-7d9b5c7b8d-f9j7s 1/1 Running 0 35s"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
prompt: TERMINAL_DEFAULTS.promptPrefix + "$",
|
||||||
|
command: "kubectl get ingress -n blog-prod",
|
||||||
|
output: [
|
||||||
|
"NAME CLASS HOSTS ADDRESS PORTS AGE",
|
||||||
|
"blog-ingress <none> blog.laforceit.com 192.168.1.50 80, 443 42s"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Predefined terminal content blocks
|
||||||
|
export const TERMINAL_CONTENT = {
|
||||||
|
fileExplorer: `<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">ls -la</span>
|
||||||
|
|
||||||
|
total 42
|
||||||
|
drwxr-xr-x 6 laforceit users 4096 Nov 7 22:15 .
|
||||||
|
drwxr-xr-x 12 laforceit users 4096 Nov 7 20:32 ..
|
||||||
|
-rw-r--r-- 1 laforceit users 182 Nov 7 22:15 .astro
|
||||||
|
drwxr-xr-x 2 laforceit users 4096 Nov 7 21:03 components
|
||||||
|
drwxr-xr-x 3 laforceit users 4096 Nov 7 21:14 content
|
||||||
|
drwxr-xr-x 4 laforceit users 4096 Nov 7 21:42 layouts
|
||||||
|
drwxr-xr-x 5 laforceit users 4096 Nov 7 22:10 pages
|
||||||
|
-rw-r--r-- 1 laforceit users 1325 Nov 7 22:12 package.json`,
|
||||||
|
|
||||||
|
tags: `<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">cat ./content/tags.txt</span>
|
||||||
|
|
||||||
|
cloudflare
|
||||||
|
coding
|
||||||
|
containers
|
||||||
|
devops
|
||||||
|
digital-garden
|
||||||
|
docker
|
||||||
|
file-management
|
||||||
|
filebrowser
|
||||||
|
flux
|
||||||
|
git
|
||||||
|
gitea
|
||||||
|
gitops
|
||||||
|
grafana
|
||||||
|
homelab
|
||||||
|
infrastructure
|
||||||
|
k3s
|
||||||
|
knowledge-management
|
||||||
|
kubernetes
|
||||||
|
learning-in-public
|
||||||
|
monitoring
|
||||||
|
networking
|
||||||
|
observability
|
||||||
|
obsidian
|
||||||
|
prometheus
|
||||||
|
proxmox
|
||||||
|
quartz
|
||||||
|
rancher
|
||||||
|
remote-development
|
||||||
|
security
|
||||||
|
self-hosted
|
||||||
|
terraform
|
||||||
|
test
|
||||||
|
tunnels
|
||||||
|
tutorial
|
||||||
|
virtualization
|
||||||
|
vscode`,
|
||||||
|
|
||||||
|
blogDeployment: `<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">git add src/content/posts/kubernetes-monitoring.md</span>
|
||||||
|
|
||||||
|
<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">git commit -m "feat: add new article on Kubernetes monitoring"</span>
|
||||||
|
[main <span class="term-green">8fd43a9</span>] <span class="term-cyan">feat: add new article on Kubernetes monitoring</span>
|
||||||
|
1 file changed, 147 insertions(+)
|
||||||
|
create mode 100644 src/content/posts/kubernetes-monitoring.md
|
||||||
|
|
||||||
|
<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">git push origin main</span>
|
||||||
|
Enumerating objects: 8, done.
|
||||||
|
Counting objects: 100% (8/8), done.
|
||||||
|
Delta compression using up to 8 threads
|
||||||
|
Compressing objects: 100% (5/5), done.
|
||||||
|
Writing objects: 100% (5/5), 2.12 KiB | 2.12 MiB/s, done.
|
||||||
|
Total 5 (delta 3), reused 0 (delta 0), pack-reused 0
|
||||||
|
remote: Resolving deltas: 100% (3/3), completed with 3 local objects.
|
||||||
|
<span class="term-green">✓</span> Deployed to https://laforceit.com
|
||||||
|
<span class="term-green">✓</span> Article published successfully`,
|
||||||
|
|
||||||
|
k8sInstall: `<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">curl -sfL https://get.k3s.io | sh -</span>
|
||||||
|
[INFO] Finding release for channel stable
|
||||||
|
[INFO] Using v1.27.4+k3s1 as release
|
||||||
|
[INFO] Downloading hash https://github.com/k3s-io/k3s/releases/download/v1.27.4+k3s1/sha256sum-amd64.txt
|
||||||
|
[INFO] Downloading binary https://github.com/k3s-io/k3s/releases/download/v1.27.4+k3s1/k3s
|
||||||
|
[INFO] Verifying binary download
|
||||||
|
[INFO] Installing k3s to /usr/local/bin/k3s
|
||||||
|
[INFO] Creating /usr/local/bin/kubectl symlink to k3s
|
||||||
|
[INFO] Creating /usr/local/bin/crictl symlink to k3s
|
||||||
|
[INFO] Creating /usr/local/bin/ctr symlink to k3s
|
||||||
|
[INFO] Creating killall script /usr/local/bin/k3s-killall.sh
|
||||||
|
[INFO] Creating uninstall script /usr/local/bin/k3s-uninstall.sh
|
||||||
|
[INFO] env: Creating environment file /etc/systemd/system/k3s.service.env
|
||||||
|
[INFO] systemd: Creating service file /etc/systemd/system/k3s.service
|
||||||
|
[INFO] systemd: Enabling k3s unit
|
||||||
|
Created symlink /etc/systemd/system/multi-user.target.wants/k3s.service → /etc/systemd/system/k3s.service.
|
||||||
|
[INFO] systemd: Starting k3s
|
||||||
|
<span class="term-green">✓</span> K3s has been installed successfully
|
||||||
|
|
||||||
|
<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">kubectl get pods -A</span>
|
||||||
|
NAMESPACE NAME READY STATUS RESTARTS AGE
|
||||||
|
kube-system helm-install-traefik-crd-k7gxl 0/1 Completed 0 2m43s
|
||||||
|
kube-system helm-install-traefik-pvvhg 0/1 Completed 1 2m43s
|
||||||
|
kube-system metrics-server-67c658dc48-mxnxp 1/1 Running 0 2m43s
|
||||||
|
kube-system local-path-provisioner-7b7dc8d6f5-q99nl 1/1 Running 0 2m43s
|
||||||
|
kube-system coredns-b96499967-nkvnz 1/1 Running 0 2m43s
|
||||||
|
kube-system svclb-traefik-bd0bfb17-ht8gq 2/2 Running 0 96s
|
||||||
|
kube-system traefik-7d586bdc47-d6lzr 1/1 Running 0 96s`,
|
||||||
|
|
||||||
|
dockerCompose: `<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">cat docker-compose.yaml</span>
|
||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
blog:
|
||||||
|
image: node:18-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
|
working_dir: /app
|
||||||
|
command: sh -c "npm install && npm run dev"
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=development
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:14-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
- POSTGRES_PASSWORD=secure_password
|
||||||
|
- POSTGRES_USER=bloguser
|
||||||
|
- POSTGRES_DB=blogdb
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
|
<div class="term-blue">${TERMINAL_DEFAULTS.promptPrefix}</div><span>$</span> <span class="term-bold">docker-compose up -d</span>
|
||||||
|
Creating network "laforceit-blog_default" with the default driver
|
||||||
|
Creating volume "laforceit-blog_postgres_data" with default driver
|
||||||
|
Pulling blog (node:18-alpine)...
|
||||||
|
Pulling db (postgres:14-alpine)...
|
||||||
|
Creating laforceit-blog_db_1 ... done
|
||||||
|
Creating laforceit-blog_blog_1 ... done`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to create terminal presets
|
||||||
|
export function createTerminalPreset(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'blog-search':
|
||||||
|
return BLOG_SEARCH_SEQUENCE[Math.floor(Math.random() * BLOG_SEARCH_SEQUENCE.length)];
|
||||||
|
|
||||||
|
case 'system-monitor':
|
||||||
|
return SYSTEM_MONITOR_SEQUENCE[Math.floor(Math.random() * SYSTEM_MONITOR_SEQUENCE.length)];
|
||||||
|
|
||||||
|
case 'blog-deploy':
|
||||||
|
return BLOG_DEPLOYMENT_SEQUENCE[Math.floor(Math.random() * BLOG_DEPLOYMENT_SEQUENCE.length)];
|
||||||
|
|
||||||
|
case 'k8s-ops':
|
||||||
|
return K8S_OPERATION_SEQUENCE[Math.floor(Math.random() * K8S_OPERATION_SEQUENCE.length)];
|
||||||
|
|
||||||
|
case 'k8s':
|
||||||
|
return {
|
||||||
|
title: "argobox:~/kubernetes",
|
||||||
|
command: "kubectl get pods -A",
|
||||||
|
output: `NAMESPACE NAME READY STATUS RESTARTS AGE
|
||||||
|
kube-system coredns-66bff467f8-8p7z2 1/1 Running 0 15d
|
||||||
|
kube-system coredns-66bff467f8-v68vr 1/1 Running 0 15d
|
||||||
|
kube-system etcd-control-plane 1/1 Running 0 15d
|
||||||
|
kube-system kube-apiserver-control-plane 1/1 Running 0 15d
|
||||||
|
kube-system kube-controller-manager-control-plane 1/1 Running 0 15d
|
||||||
|
kube-system kube-proxy-c84qf 1/1 Running 0 15d
|
||||||
|
kube-system kube-scheduler-control-plane 1/1 Running 0 15d`
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'docker':
|
||||||
|
return {
|
||||||
|
title: "argobox:~/docker",
|
||||||
|
command: "docker ps",
|
||||||
|
output: `CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
|
||||||
|
d834f0efcf2f nginx:latest "/docker-entrypoint.…" Up 2 days 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp web
|
||||||
|
0b292940b4c0 postgres:13 "docker-entrypoint.s…" Up 2 days 0.0.0.0:5432->5432/tcp db
|
||||||
|
a834fa3ede06 redis:6 "docker-entrypoint.s…" Up 2 days 0.0.0.0:6379->6379/tcp cache`
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'search':
|
||||||
|
return {
|
||||||
|
title: "argobox:~/blog",
|
||||||
|
command: "grep -r \"kubernetes\" --include=\"*.md\" ./posts | wc -l",
|
||||||
|
output: "7 matches found"
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'random-cool':
|
||||||
|
// Pick a random sequence for a cool effect
|
||||||
|
const sequences = [
|
||||||
|
TERMINAL_CONTENT.k8sInstall,
|
||||||
|
TERMINAL_CONTENT.blogDeployment,
|
||||||
|
TERMINAL_CONTENT.dockerCompose,
|
||||||
|
...BLOG_SEARCH_SEQUENCE.map(item => `<div class="term-blue">${item.prompt}</div><span>$</span> <span class="term-bold">${item.command}</span>\n${item.output.join('\n')}`),
|
||||||
|
...SYSTEM_MONITOR_SEQUENCE.map(item => `<div class="term-blue">${item.prompt}</div><span>$</span> <span class="term-bold">${item.command}</span>\n${item.output.join('\n')}`),
|
||||||
|
...BLOG_DEPLOYMENT_SEQUENCE.map(item => `<div class="term-blue">${item.prompt}</div><span>$</span> <span class="term-bold">${item.command}</span>\n${item.output.join('\n')}`),
|
||||||
|
...K8S_OPERATION_SEQUENCE.map(item => `<div class="term-blue">${item.prompt}</div><span>$</span> <span class="term-bold">${item.command}</span>\n${item.output.join('\n')}`)
|
||||||
|
];
|
||||||
|
return {
|
||||||
|
title: "argobox:~/cool-stuff",
|
||||||
|
content: sequences[Math.floor(Math.random() * sequences.length)]
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
title: TERMINAL_DEFAULTS.title,
|
||||||
|
command: "echo 'Hello from LaForceIT Terminal'",
|
||||||
|
output: "Hello from LaForceIT Terminal"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,7 +57,7 @@ const {
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
|
||||||
<!-- Theme CSS -->
|
<!-- Theme CSS -->
|
||||||
<link rel="stylesheet" href="/styles/theme.css" />
|
<link rel="stylesheet" href="/src/styles/theme.css" />
|
||||||
|
|
||||||
<!-- Cytoscape Library for Knowledge Graph -->
|
<!-- Cytoscape Library for Knowledge Graph -->
|
||||||
<script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>
|
<script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>
|
||||||
|
@ -360,5 +360,90 @@ const {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Add copy to clipboard functionality for code blocks -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Find all code blocks
|
||||||
|
const codeBlocks = document.querySelectorAll('pre code');
|
||||||
|
|
||||||
|
// Add copy button to each
|
||||||
|
codeBlocks.forEach((codeBlock, index) => {
|
||||||
|
// Create container for copy button (to enable positioning)
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'code-block-container';
|
||||||
|
container.style.position = 'relative';
|
||||||
|
|
||||||
|
// Create copy button
|
||||||
|
const copyButton = document.createElement('button');
|
||||||
|
copyButton.className = 'copy-code-button';
|
||||||
|
copyButton.setAttribute('aria-label', 'Copy code to clipboard');
|
||||||
|
copyButton.innerHTML = `
|
||||||
|
<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" class="copy-icon">
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</svg>
|
||||||
|
<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" class="check-icon" style="display: none;">
|
||||||
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Style the button
|
||||||
|
copyButton.style.position = 'absolute';
|
||||||
|
copyButton.style.top = '0.5rem';
|
||||||
|
copyButton.style.right = '0.5rem';
|
||||||
|
copyButton.style.padding = '0.25rem';
|
||||||
|
copyButton.style.background = 'rgba(45, 55, 72, 0.5)';
|
||||||
|
copyButton.style.border = '1px solid rgba(255, 255, 255, 0.2)';
|
||||||
|
copyButton.style.borderRadius = '0.25rem';
|
||||||
|
copyButton.style.cursor = 'pointer';
|
||||||
|
copyButton.style.zIndex = '10';
|
||||||
|
copyButton.style.opacity = '0';
|
||||||
|
copyButton.style.transition = 'opacity 0.2s';
|
||||||
|
|
||||||
|
// Add click handler
|
||||||
|
copyButton.addEventListener('click', () => {
|
||||||
|
// Get code text
|
||||||
|
const code = codeBlock.textContent;
|
||||||
|
|
||||||
|
// Copy to clipboard
|
||||||
|
navigator.clipboard.writeText(code).then(() => {
|
||||||
|
// Show success UI
|
||||||
|
copyButton.querySelector('.copy-icon').style.display = 'none';
|
||||||
|
copyButton.querySelector('.check-icon').style.display = 'block';
|
||||||
|
|
||||||
|
// Reset after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
copyButton.querySelector('.copy-icon').style.display = 'block';
|
||||||
|
copyButton.querySelector('.check-icon').style.display = 'none';
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clone the code block
|
||||||
|
const preElement = codeBlock.parentElement;
|
||||||
|
const wrapper = preElement.parentElement;
|
||||||
|
|
||||||
|
// Create the container structure
|
||||||
|
container.appendChild(preElement.cloneNode(true));
|
||||||
|
container.appendChild(copyButton);
|
||||||
|
|
||||||
|
// Replace the original pre with our container
|
||||||
|
wrapper.replaceChild(container, preElement);
|
||||||
|
|
||||||
|
// Update the reference to the new code block
|
||||||
|
const newCodeBlock = container.querySelector('code');
|
||||||
|
|
||||||
|
// Add hover behavior
|
||||||
|
container.addEventListener('mouseenter', () => {
|
||||||
|
copyButton.style.opacity = '1';
|
||||||
|
});
|
||||||
|
|
||||||
|
container.addEventListener('mouseleave', () => {
|
||||||
|
copyButton.style.opacity = '0';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,8 @@
|
||||||
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 { getCollection } from 'astro:content';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
frontmatter: {
|
frontmatter: {
|
||||||
|
@ -11,20 +12,73 @@ interface Props {
|
||||||
pubDate: Date;
|
pubDate: Date;
|
||||||
updatedDate?: Date;
|
updatedDate?: Date;
|
||||||
heroImage?: string;
|
heroImage?: string;
|
||||||
category?: string; // Keep category for potential filtering, but don't display in header
|
category?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
readTime?: string;
|
readTime?: string;
|
||||||
draft?: boolean;
|
draft?: boolean;
|
||||||
author?: string; // Keep author field if needed elsewhere
|
author?: string;
|
||||||
// Add other potential frontmatter fields as optional
|
// Field for explicitly related posts
|
||||||
github?: string;
|
related_posts?: string[];
|
||||||
live?: string;
|
},
|
||||||
technologies?: string[];
|
slug: string // Add slug to props
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { frontmatter } = Astro.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', {
|
const formattedPubDate = frontmatter.pubDate ? new Date(frontmatter.pubDate).toLocaleDateString('en-us', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'long',
|
month: 'long',
|
||||||
|
@ -47,10 +101,10 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
||||||
<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 */}
|
||||||
|
@ -63,7 +117,7 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
||||||
<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>}
|
||||||
{/* Category removed from display here */}
|
{frontmatter.category && <span class="blog-post-category">{frontmatter.category}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tags */}
|
{/* Tags */}
|
||||||
|
@ -83,93 +137,199 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
||||||
</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>
|
||||||
|
|
||||||
{/* Future Feature Placeholders remain commented out */}
|
{/* 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>
|
||||||
{/* Social links removed */}
|
<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>
|
||||||
|
|
||||||
{/* Future Feature Placeholders remain commented out */}
|
|
||||||
{/* ... */}
|
|
||||||
</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) {
|
|
||||||
const tocList = document.createElement('ul');
|
if (headings.length === 0) {
|
||||||
tocList.className = 'toc-list';
|
tocContainer.innerHTML = '<p class="toc-empty">No sections found in this article.</p>';
|
||||||
headings.forEach((heading) => {
|
return;
|
||||||
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') {
|
// Create the TOC list
|
||||||
document.addEventListener('DOMContentLoaded', generateToc);
|
const tocList = document.createElement('ul');
|
||||||
} else {
|
tocList.className = 'toc-list';
|
||||||
generateToc();
|
|
||||||
}
|
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>
|
</script>
|
||||||
|
|
||||||
{/* Styles Updated */}
|
<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;
|
||||||
|
@ -179,98 +339,162 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
||||||
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;
|
|
||||||
/* Adjusted grid for wider TOC/Sidebar */
|
|
||||||
grid-template-columns: 7fr 2fr;
|
|
||||||
gap: 3rem; /* Wider gap */
|
|
||||||
max-width: 1400px; /* Wider max width */
|
|
||||||
margin: 2rem auto;
|
|
||||||
padding: 0 1.5rem;
|
|
||||||
}
|
|
||||||
.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.2;
|
||||||
line-height: 1.25; /* Adjusted line height */
|
margin-bottom: 0.75rem;
|
||||||
margin-bottom: 0.75rem; /* Adjusted margin */
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
/* Removed .blog-post-category style */
|
|
||||||
|
.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;
|
||||||
}
|
}
|
||||||
.blog-post-content {
|
|
||||||
/* Styles inherited from prose */
|
/* 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 {
|
.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;
|
||||||
|
@ -278,69 +502,94 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
margin: 0 auto 1rem;
|
margin: 0 auto 1rem;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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>
|
|
@ -215,52 +215,55 @@ const commands = [
|
||||||
});
|
});
|
||||||
|
|
||||||
// Function to create HTML for a single post card
|
// Function to create HTML for a single post card
|
||||||
function createPostCardHTML(post) {
|
// Update the post card HTML creation function in the blog/index.astro file
|
||||||
// Make sure tags is an array before stringifying
|
// Find the function that creates post cards (might be called createPostCardHTML)
|
||||||
const tagsString = JSON.stringify(post.tags || []);
|
|
||||||
|
function createPostCardHTML(post) {
|
||||||
// Create tag pills HTML
|
// Make sure tags is an array before stringifying
|
||||||
const tagPills = post.tags.map(tag =>
|
const tagsString = JSON.stringify(post.tags || []);
|
||||||
`<span class="post-tag" data-tag="${tag}">${tag}</span>`
|
|
||||||
).join('');
|
// Create tag pills HTML
|
||||||
|
const tagPills = post.tags.map(tag =>
|
||||||
return `
|
`<span class="post-tag" data-tag="${tag}">${tag}</span>`
|
||||||
<article class="post-card" data-tags='${tagsString}' data-slug="${post.slug}">
|
).join('');
|
||||||
<div class="post-card-inner">
|
|
||||||
<a href="/posts/${post.slug}/" class="post-image-link">
|
return `
|
||||||
<div class="post-image-container">
|
<article class="post-card" data-tags='${tagsString}' data-slug="${post.slug}">
|
||||||
<img
|
<div class="post-card-inner">
|
||||||
width="720"
|
<a href="/posts/${post.slug}/" class="post-image-link">
|
||||||
height="360"
|
<div class="post-image-container">
|
||||||
src="${post.heroImage}"
|
<img
|
||||||
alt=""
|
width="720"
|
||||||
class="post-image"
|
height="360"
|
||||||
loading="lazy"
|
src="${post.heroImage}"
|
||||||
/>
|
alt=""
|
||||||
<div class="post-category-badge">${post.category}</div>
|
class="post-image"
|
||||||
</div>
|
loading="lazy"
|
||||||
</a>
|
/>
|
||||||
<div class="post-content">
|
<div class="post-category-badge">${post.category}</div>
|
||||||
<div class="post-meta">
|
|
||||||
<time datetime="${post.pubDateISO}">${post.pubDate}</time>
|
|
||||||
<span class="post-read-time">${post.readTime}</span>
|
|
||||||
</div>
|
|
||||||
<h3 class="post-title">
|
|
||||||
<a href="/posts/${post.slug}/">${post.title}</a>
|
|
||||||
${post.isDraft ? '<span class="draft-badge">Draft</span>' : ''}
|
|
||||||
</h3>
|
|
||||||
<p class="post-excerpt">${post.description}</p>
|
|
||||||
<div class="post-footer">
|
|
||||||
<div class="post-tags">
|
|
||||||
${tagPills}
|
|
||||||
</div>
|
|
||||||
<a href="/posts/${post.slug}/" class="read-more">Read More</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</a>
|
||||||
`;
|
<div class="post-content">
|
||||||
}
|
<div class="post-meta">
|
||||||
|
<time datetime="${post.pubDateISO}">${post.pubDate}</time>
|
||||||
|
<span class="post-read-time">${post.readTime}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="post-title">
|
||||||
|
<a href="/posts/${post.slug}/">${post.title}</a>
|
||||||
|
${post.isDraft ? '<span class="draft-badge">Draft</span>' : ''}
|
||||||
|
</h3>
|
||||||
|
<p class="post-excerpt">${post.description}</p>
|
||||||
|
<div class="post-footer">
|
||||||
|
<div class="post-tags">
|
||||||
|
${tagPills}
|
||||||
|
</div>
|
||||||
|
<a href="/posts/${post.slug}/" class="read-more">Read More</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// Function to filter and update the grid
|
// Function to filter and update the grid
|
||||||
function updateGrid() {
|
function updateGrid() {
|
||||||
|
@ -949,4 +952,34 @@ const commands = [
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add CSS to make the image link more obvious on hover */
|
||||||
|
.post-image-link {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px 8px 0 0; /* Match card radius */
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image-link:hover .post-image {
|
||||||
|
transform: scale(1.05);
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image-link::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(6, 182, 212, 0.1); /* Use accent color with alpha */
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
pointer-events: none; /* Allow clicks through */
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image-link:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,10 +1,11 @@
|
||||||
---
|
---
|
||||||
|
// src/pages/blog/index.astro - Blog page with enhanced knowledge graph and filtering
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro'; // Corrected path
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
import KnowledgeGraph from '../components/KnowledgeGraph.astro'; // Corrected path
|
import KnowledgeGraph from '../components/KnowledgeGraph.astro';
|
||||||
import Terminal from '../components/Terminal.astro'; // Corrected path
|
import Terminal from '../components/Terminal.astro';
|
||||||
import Header from '../components/Header.astro'; // Import Header
|
import Header from '../components/Header.astro';
|
||||||
import Footer from '../components/Footer.astro'; // Import Footer
|
import Footer from '../components/Footer.astro';
|
||||||
|
|
||||||
// Get all blog entries
|
// Get all blog entries
|
||||||
const allPosts = await getCollection('posts');
|
const allPosts = await getCollection('posts');
|
||||||
|
@ -31,67 +32,87 @@ const postsData = sortedPosts.map(post => ({
|
||||||
pubDateISO: post.data.pubDate ? new Date(post.data.pubDate).toISOString() : '',
|
pubDateISO: post.data.pubDate ? new Date(post.data.pubDate).toISOString() : '',
|
||||||
category: post.data.category || 'Uncategorized',
|
category: post.data.category || 'Uncategorized',
|
||||||
tags: post.data.tags || [],
|
tags: post.data.tags || [],
|
||||||
heroImage: post.data.heroImage || '/images/placeholders/default.jpg',
|
heroImage: post.data.heroImage || '/images/placeholders/default.jpg',
|
||||||
readTime: post.data.readTime || '5 min read',
|
readTime: post.data.readTime || '5 min read',
|
||||||
isDraft: post.data.draft || false
|
isDraft: post.data.draft || false
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Prepare graph data (Obsidian-style: Posts and Tags)
|
// Prepare enhanced graph data with both posts and tags
|
||||||
const graphNodes = [];
|
const graphData = {
|
||||||
const graphEdges = [];
|
nodes: [
|
||||||
const tagNodes = new Map(); // To avoid duplicate tag nodes
|
// Add post nodes
|
||||||
|
...sortedPosts
|
||||||
|
.filter(post => !post.data.draft)
|
||||||
|
.map(post => ({
|
||||||
|
id: post.slug,
|
||||||
|
label: post.data.title,
|
||||||
|
type: 'post',
|
||||||
|
category: post.data.category || 'Uncategorized',
|
||||||
|
tags: post.data.tags || [],
|
||||||
|
url: `/posts/${post.slug}/`
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Add tag nodes
|
||||||
|
...allTags.map(tag => ({
|
||||||
|
id: `tag-${tag}`,
|
||||||
|
label: tag,
|
||||||
|
type: 'tag',
|
||||||
|
url: `/tag/${tag}/`
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
edges: []
|
||||||
|
};
|
||||||
|
|
||||||
// Add post nodes
|
// Create edges between posts and their tags
|
||||||
sortedPosts.forEach(post => {
|
sortedPosts
|
||||||
if (!post.data.draft) { // Exclude drafts from graph
|
.filter(post => !post.data.draft)
|
||||||
graphNodes.push({
|
.forEach(post => {
|
||||||
id: post.slug,
|
const postTags = post.data.tags || [];
|
||||||
label: post.data.title,
|
|
||||||
type: 'post', // Add type for styling/interaction
|
// Add edges from post to tags
|
||||||
url: `/posts/${post.slug}/` // Add URL for linking
|
postTags.forEach(tag => {
|
||||||
});
|
graphData.edges.push({
|
||||||
|
|
||||||
// Add tag nodes and edges
|
|
||||||
(post.data.tags || []).forEach(tag => {
|
|
||||||
const tagId = `tag-${tag}`;
|
|
||||||
// Add tag node only if it doesn't exist
|
|
||||||
if (!tagNodes.has(tagId)) {
|
|
||||||
graphNodes.push({
|
|
||||||
id: tagId,
|
|
||||||
label: `#${tag}`, // Prefix with # for clarity
|
|
||||||
type: 'tag' // Add type
|
|
||||||
});
|
|
||||||
tagNodes.set(tagId, true);
|
|
||||||
}
|
|
||||||
// Add edge connecting post to tag
|
|
||||||
graphEdges.push({
|
|
||||||
source: post.slug,
|
source: post.slug,
|
||||||
target: tagId,
|
target: `tag-${tag}`,
|
||||||
type: 'tag-connection' // Add type
|
type: 'post-tag',
|
||||||
|
strength: 1
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
// Check if post references other posts (optional)
|
||||||
|
// This requires a related_posts field in frontmatter
|
||||||
const graphData = { nodes: graphNodes, edges: graphEdges };
|
if (post.data.related_posts && Array.isArray(post.data.related_posts)) {
|
||||||
|
post.data.related_posts.forEach(relatedSlug => {
|
||||||
|
// Make sure related post exists
|
||||||
|
if (sortedPosts.some(p => p.slug === relatedSlug)) {
|
||||||
|
graphData.edges.push({
|
||||||
|
source: post.slug,
|
||||||
|
target: relatedSlug,
|
||||||
|
type: 'post-post',
|
||||||
|
strength: 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Terminal commands for tech effect
|
// Terminal commands for tech effect
|
||||||
const commands = [
|
const commands = [
|
||||||
{
|
{
|
||||||
prompt: "[laforceit@argobox]$ ",
|
prompt: "[laforceit@argobox]$ ",
|
||||||
command: "find ./posts -type f -name \"*.md\" | sort -n | wc -l",
|
command: "find ./posts -type f -name \"*.md\" | sort -n | wc -l",
|
||||||
output: [`${allPosts.length} posts found`]
|
output: [`${allPosts.length} posts found`]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prompt: "[laforceit@argobox]$ ",
|
prompt: "[laforceit@argobox]$ ",
|
||||||
command: "ls -la ./categories",
|
command: "ls -la ./tags",
|
||||||
output: allCategories.map(cat => `${cat}`)
|
output: allTags.map(tag => `${tag}`)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
prompt: "[laforceit@argobox]$ ",
|
prompt: "[laforceit@argobox]$ ",
|
||||||
command: "grep -r \"kubernetes\" --include=\"*.md\" ./posts | wc -l",
|
command: "grep -r \"kubernetes\" --include=\"*.md\" ./posts | wc -l",
|
||||||
output: [`${allPosts.filter(post =>
|
output: [`${allPosts.filter(post =>
|
||||||
post.data.tags?.includes('kubernetes') ||
|
post.data.tags?.includes('kubernetes') ||
|
||||||
post.data.category === 'Kubernetes' ||
|
post.data.category === 'Kubernetes' ||
|
||||||
post.data.title?.toLowerCase().includes('kubernetes') ||
|
post.data.title?.toLowerCase().includes('kubernetes') ||
|
||||||
post.data.description?.toLowerCase().includes('kubernetes')
|
post.data.description?.toLowerCase().includes('kubernetes')
|
||||||
|
@ -101,10 +122,11 @@ const commands = [
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Blog | LaForce IT - Home Lab & DevOps Insights" description="Explore articles about Kubernetes, Infrastructure, DevOps, and Home Lab setups">
|
<BaseLayout title="Blog | LaForce IT - Home Lab & DevOps Insights" description="Explore articles about Kubernetes, Infrastructure, DevOps, and Home Lab setups">
|
||||||
<Header slot="header" /> {/* Pass Header to slot */}
|
<Header slot="header" />
|
||||||
<main>
|
<main>
|
||||||
{/* Hero Section with Terminal */}
|
<!-- Hero Section with Terminal -->
|
||||||
<section class="hero-section">
|
<section class="hero-section">
|
||||||
|
<div class="hero-bg"></div>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="hero-content">
|
<div class="hero-content">
|
||||||
<div class="hero-text">
|
<div class="hero-text">
|
||||||
|
@ -121,46 +143,58 @@ const commands = [
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Blog Posts Section */}
|
<!-- Blog Content Section -->
|
||||||
<section class="blog-posts-section">
|
<section class="blog-content-section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-header">
|
<!-- Search and Filter Section with integrated Knowledge Graph -->
|
||||||
<h2 class="section-title">Latest Articles</h2>
|
<div class="search-filter-container">
|
||||||
<p class="section-description">
|
<div class="section-header">
|
||||||
Technical insights, infrastructure guides, and DevOps best practices
|
<h2 class="section-title">Knowledge Graph & Content Explorer</h2>
|
||||||
</p>
|
<p class="section-description">
|
||||||
</div>
|
Explore connections between articles and topics, or search by keyword
|
||||||
|
</p>
|
||||||
{/* Search and Filter Section */}
|
|
||||||
<div class="search-filter-section">
|
|
||||||
<div class="search-bar">
|
|
||||||
<input type="search" id="search-input" placeholder="Search posts..." class="search-input" />
|
|
||||||
</div>
|
</div>
|
||||||
<span class="filter-label">Filter by Tag:</span>
|
|
||||||
<button class="tag-filter-btn active" data-tag="all">All</button>
|
<!-- Knowledge Graph Visualization -->
|
||||||
{allTags.map(tag => (
|
<div class="knowledge-graph-wrapper">
|
||||||
<button class="tag-filter-btn" data-tag={tag}>{tag}</button>
|
<KnowledgeGraph graphData={graphData} height="500px" />
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Integrated Knowledge Graph */}
|
<div class="search-filter-section">
|
||||||
<div class="integrated-graph-container">
|
<div class="search-bar">
|
||||||
<KnowledgeGraph graphData={graphData} />
|
<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" class="search-icon"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||||
{/* We will update graphData generation later */}
|
<input type="search" id="search-input" placeholder="Search posts..." class="search-input" />
|
||||||
|
</div>
|
||||||
|
<div class="tag-filters">
|
||||||
|
<span class="filter-label">Filter by Tag:</span>
|
||||||
|
<button class="tag-filter-btn active" data-tag="all">All</button>
|
||||||
|
{allTags.map(tag => (
|
||||||
|
<button class="tag-filter-btn" data-tag={tag}>{tag}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Blog Grid (will be populated by JS) */}
|
<!-- Blog Grid (populated by JS) -->
|
||||||
<div class="blog-grid" id="blog-grid">
|
<div class="blog-section">
|
||||||
<div class="loading-indicator">
|
<div class="section-header">
|
||||||
<div class="loading-spinner"></div>
|
<h2 class="section-title">All Articles</h2>
|
||||||
<span>Loading articles...</span>
|
<p class="section-description">
|
||||||
|
Technical insights, infrastructure guides, and DevOps best practices
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="blog-grid" id="blog-grid">
|
||||||
|
<div class="loading-indicator">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<span>Loading articles...</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<Footer slot="footer" /> {/* Pass Footer to slot */}
|
<Footer slot="footer" />
|
||||||
|
|
||||||
<!-- Client-side script for filtering and graph interactions -->
|
<!-- Client-side script for filtering and graph interactions -->
|
||||||
<script define:vars={{ postsData, graphData }}>
|
<script define:vars={{ postsData, graphData }}>
|
||||||
|
@ -168,133 +202,68 @@ const commands = [
|
||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
const tagButtons = document.querySelectorAll('.tag-filter-btn');
|
const tagButtons = document.querySelectorAll('.tag-filter-btn');
|
||||||
const blogGrid = document.getElementById('blog-grid');
|
const blogGrid = document.getElementById('blog-grid');
|
||||||
// Removed graphFilters as category filtering is removed from graph
|
|
||||||
|
|
||||||
// State variables
|
// State variables
|
||||||
let currentFilterTag = 'all';
|
let currentFilterTag = 'all';
|
||||||
let currentSearchTerm = '';
|
let currentSearchTerm = '';
|
||||||
// Removed currentGraphFilter
|
|
||||||
let cy; // Cytoscape instance will be set by KnowledgeGraph component
|
let cy; // Cytoscape instance will be set by KnowledgeGraph component
|
||||||
|
|
||||||
// Wait for cytoscape instance to be available
|
// Wait for cytoscape instance to be available
|
||||||
document.addEventListener('graphReady', (e) => {
|
document.addEventListener('graphReady', (e) => {
|
||||||
cy = e.detail.cy;
|
cy = e.detail.cy;
|
||||||
setupGraphInteractions();
|
console.log('Graph ready and connected to filtering system');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup graph interactions (Post and Tag nodes)
|
|
||||||
function setupGraphInteractions() {
|
|
||||||
if (!cy) {
|
|
||||||
console.error("Cytoscape instance not ready.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove previous category filter logic if any existed
|
|
||||||
// graphFilters.forEach(...) logic removed
|
|
||||||
|
|
||||||
// Handle clicks on graph nodes
|
|
||||||
cy.on('tap', 'node', function(evt) {
|
|
||||||
const node = evt.target;
|
|
||||||
const nodeId = node.id();
|
|
||||||
const nodeType = node.data('type'); // Get type ('post' or 'tag')
|
|
||||||
|
|
||||||
console.log(`Node clicked: ID=${nodeId}, Type=${nodeType}`); // Debug log
|
|
||||||
|
|
||||||
if (nodeType === 'post') {
|
|
||||||
// Handle post node click: Find post, update search, filter grid, scroll
|
|
||||||
const post = postsData.find(p => p.slug === nodeId);
|
|
||||||
if (post) {
|
|
||||||
console.log(`Post node clicked: ${post.title}`);
|
|
||||||
// Reset tag filter to 'all' when a specific post is selected via graph
|
|
||||||
currentFilterTag = 'all';
|
|
||||||
tagButtons.forEach(btn => btn.classList.remove('active'));
|
|
||||||
const allButton = document.querySelector('.tag-filter-btn[data-tag="all"]');
|
|
||||||
if (allButton) allButton.classList.add('active');
|
|
||||||
|
|
||||||
// Update search bar and term
|
|
||||||
searchInput.value = post.title; // Show post title in search
|
|
||||||
currentSearchTerm = post.title; // Filter grid by title
|
|
||||||
|
|
||||||
// Update grid to show only this post (or matching search term)
|
|
||||||
updateGrid();
|
|
||||||
|
|
||||||
// Scroll to the blog section smoothly
|
|
||||||
const blogSection = document.querySelector('.blog-posts-section');
|
|
||||||
if (blogSection) {
|
|
||||||
blogSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(`Post data not found for slug: ${nodeId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (nodeType === 'tag') {
|
|
||||||
// Handle tag node click: Simulate click on corresponding tag filter button
|
|
||||||
const tagName = nodeId.replace(/^tag-/, ''); // Extract tag name (remove 'tag-' prefix)
|
|
||||||
console.log(`Tag node clicked: ${tagName}`);
|
|
||||||
|
|
||||||
const correspondingButton = document.querySelector(`.tag-filter-btn[data-tag="${tagName}"]`);
|
|
||||||
|
|
||||||
if (correspondingButton) {
|
|
||||||
console.log(`Found corresponding button for tag: ${tagName}`);
|
|
||||||
// Simulate click on the button
|
|
||||||
correspondingButton.click();
|
|
||||||
// Scroll to blog section smoothly
|
|
||||||
const blogSection = document.querySelector('.blog-posts-section');
|
|
||||||
if (blogSection) {
|
|
||||||
blogSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(`Could not find tag filter button for tag: ${tagName}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to create HTML for a single post card
|
// Function to create HTML for a single post card
|
||||||
function createPostCardHTML(post) {
|
// Update the post card HTML creation function in the blog/index.astro file
|
||||||
// Make sure tags is an array before stringifying
|
// Find the function that creates post cards (might be called createPostCardHTML)
|
||||||
const tagsString = JSON.stringify(post.tags || []);
|
|
||||||
|
function createPostCardHTML(post) {
|
||||||
// Create tag pills HTML
|
// Make sure tags is an array before stringifying
|
||||||
const tagPills = post.tags.map(tag =>
|
const tagsString = JSON.stringify(post.tags || []);
|
||||||
`<span class="post-tag">${tag}</span>`
|
|
||||||
).join('');
|
// Create tag pills HTML
|
||||||
|
const tagPills = post.tags.map(tag =>
|
||||||
return `
|
`<span class="post-tag" data-tag="${tag}">${tag}</span>`
|
||||||
<article class="post-card" data-tags='${tagsString}' data-slug="${post.slug}">
|
).join('');
|
||||||
<div class="post-card-inner">
|
|
||||||
<div class="post-image-container">
|
return `
|
||||||
<img
|
<article class="post-card" data-tags='${tagsString}' data-slug="${post.slug}">
|
||||||
width="720"
|
<div class="post-card-inner">
|
||||||
height="360"
|
<a href="/posts/${post.slug}/" class="post-image-link">
|
||||||
src="${post.heroImage}"
|
<div class="post-image-container">
|
||||||
alt=""
|
<img
|
||||||
class="post-image"
|
width="720"
|
||||||
loading="lazy"
|
height="360"
|
||||||
/>
|
src="${post.heroImage}"
|
||||||
<div class="post-category-badge">${post.category}</div>
|
alt=""
|
||||||
</div>
|
class="post-image"
|
||||||
<div class="post-content">
|
loading="lazy"
|
||||||
<div class="post-meta">
|
/>
|
||||||
<time datetime="${post.pubDateISO}">${post.pubDate}</time>
|
<div class="post-category-badge">${post.category}</div>
|
||||||
<span class="post-read-time">${post.readTime}</span>
|
|
||||||
</div>
|
|
||||||
<h3 class="post-title">
|
|
||||||
<a href="/posts/${post.slug}/">${post.title}</a>
|
|
||||||
${post.isDraft ? '<span class="draft-badge">Draft</span>' : ''}
|
|
||||||
</h3>
|
|
||||||
<p class="post-excerpt">${post.description}</p>
|
|
||||||
<div class="post-footer">
|
|
||||||
<div class="post-tags">
|
|
||||||
${tagPills}
|
|
||||||
</div>
|
|
||||||
<a href="/posts/${post.slug}/" class="read-more">Read More</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</a>
|
||||||
`;
|
<div class="post-content">
|
||||||
}
|
<div class="post-meta">
|
||||||
|
<time datetime="${post.pubDateISO}">${post.pubDate}</time>
|
||||||
|
<span class="post-read-time">${post.readTime}</span>
|
||||||
|
</div>
|
||||||
|
<h3 class="post-title">
|
||||||
|
<a href="/posts/${post.slug}/">${post.title}</a>
|
||||||
|
${post.isDraft ? '<span class="draft-badge">Draft</span>' : ''}
|
||||||
|
</h3>
|
||||||
|
<p class="post-excerpt">${post.description}</p>
|
||||||
|
<div class="post-footer">
|
||||||
|
<div class="post-tags">
|
||||||
|
${tagPills}
|
||||||
|
</div>
|
||||||
|
<a href="/posts/${post.slug}/" class="read-more">Read More</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
// Function to filter and update the grid
|
// Function to filter and update the grid
|
||||||
function updateGrid() {
|
function updateGrid() {
|
||||||
|
@ -314,7 +283,7 @@ const commands = [
|
||||||
post.title.toLowerCase().includes(searchTermLower) ||
|
post.title.toLowerCase().includes(searchTermLower) ||
|
||||||
post.description.toLowerCase().includes(searchTermLower) ||
|
post.description.toLowerCase().includes(searchTermLower) ||
|
||||||
postTags.some(tag => tag.toLowerCase().includes(searchTermLower));
|
postTags.some(tag => tag.toLowerCase().includes(searchTermLower));
|
||||||
return matchesTag && matchesSearch && !post.isDraft; // Exclude drafts
|
return matchesTag && matchesSearch;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the grid HTML
|
// Update the grid HTML
|
||||||
|
@ -322,46 +291,73 @@ const commands = [
|
||||||
if (filteredPosts.length > 0) {
|
if (filteredPosts.length > 0) {
|
||||||
blogGrid.innerHTML = filteredPosts.map(createPostCardHTML).join('');
|
blogGrid.innerHTML = filteredPosts.map(createPostCardHTML).join('');
|
||||||
|
|
||||||
// If graph is available, highlight post nodes shown in the grid
|
// Add click handlers to post tag spans
|
||||||
|
document.querySelectorAll('.post-tag').forEach(tagSpan => {
|
||||||
|
tagSpan.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const tag = tagSpan.dataset.tag;
|
||||||
|
|
||||||
|
// Find and click the matching tag filter button
|
||||||
|
const tagBtn = Array.from(tagButtons).find(btn => btn.dataset.tag === tag);
|
||||||
|
if (tagBtn) {
|
||||||
|
tagBtn.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// If graph is available, highlight matching nodes
|
||||||
if (cy) {
|
if (cy) {
|
||||||
const matchingPostSlugs = filteredPosts.map(post => post.slug);
|
// Get matching slugs for posts
|
||||||
|
const matchingSlugs = filteredPosts.map(post => post.slug);
|
||||||
|
|
||||||
// Reset styles on all nodes first
|
if (currentFilterTag !== 'all') {
|
||||||
cy.nodes().removeClass('highlighted').removeClass('faded');
|
// We're filtering by tag - highlight tag node and connected posts
|
||||||
|
cy.elements().addClass('faded').removeClass('highlighted filtered');
|
||||||
// Highlight post nodes that are currently visible in the grid
|
|
||||||
cy.nodes('[type="post"]').forEach(node => {
|
// Highlight the tag node
|
||||||
if (matchingPostSlugs.includes(node.id())) {
|
const tagNode = cy.getElementById(`tag-${currentFilterTag}`);
|
||||||
node.removeClass('faded').addClass('highlighted');
|
if (tagNode.length > 0) {
|
||||||
} else {
|
tagNode.removeClass('faded').addClass('highlighted');
|
||||||
node.removeClass('highlighted').addClass('faded'); // Fade non-matching posts
|
|
||||||
|
// Get connected posts and highlight them
|
||||||
|
const connectedPosts = tagNode.neighborhood('node[type="post"]');
|
||||||
|
connectedPosts.removeClass('faded').addClass('filtered');
|
||||||
|
|
||||||
|
// Highlight connecting edges
|
||||||
|
tagNode.connectedEdges().removeClass('faded').addClass('highlighted');
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
else if (currentSearchTerm) {
|
||||||
// Highlight tag nodes connected to visible posts OR the currently selected tag
|
// We're searching - highlight matching posts
|
||||||
cy.nodes('[type="tag"]').forEach(tagNode => {
|
cy.elements().addClass('faded').removeClass('highlighted filtered');
|
||||||
const tagName = tagNode.id().replace(/^tag-/, '');
|
|
||||||
const isSelectedTag = tagName === currentFilterTag;
|
// Find and highlight matching post nodes
|
||||||
const isConnectedToVisiblePost = tagNode.connectedEdges().sources().some(postNode => matchingPostSlugs.includes(postNode.id()));
|
matchingSlugs.forEach(slug => {
|
||||||
|
const node = cy.getElementById(slug);
|
||||||
if (isSelectedTag || (currentFilterTag === 'all' && isConnectedToVisiblePost)) {
|
if (node.length > 0) {
|
||||||
tagNode.removeClass('faded').addClass('highlighted');
|
node.removeClass('faded').addClass('highlighted');
|
||||||
} else {
|
|
||||||
tagNode.removeClass('highlighted').addClass('faded');
|
// Also show connected tags
|
||||||
}
|
const connectedTags = node.neighborhood('node[type="tag"]');
|
||||||
});
|
connectedTags.removeClass('faded').addClass('filtered');
|
||||||
|
|
||||||
// Adjust edge visibility based on connected highlighted nodes
|
// And highlight edges
|
||||||
cy.edges().forEach(edge => {
|
node.connectedEdges().removeClass('faded');
|
||||||
if (edge.source().hasClass('highlighted') && edge.target().hasClass('highlighted')) {
|
|
||||||
edge.removeClass('faded').addClass('highlighted');
|
|
||||||
} else {
|
|
||||||
edge.removeClass('highlighted').addClass('faded');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Reset graph view
|
||||||
|
cy.elements().removeClass('faded highlighted filtered');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blogGrid.innerHTML = '<p class="no-results">No posts found matching your criteria.</p>';
|
blogGrid.innerHTML = '<p class="no-results">No posts found matching your criteria. Try adjusting your search or filters.</p>';
|
||||||
|
|
||||||
|
// Reset graph view
|
||||||
|
if (cy) {
|
||||||
|
cy.elements().removeClass('faded highlighted filtered');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error("Blog grid element not found!");
|
console.error("Blog grid element not found!");
|
||||||
|
@ -388,12 +384,53 @@ const commands = [
|
||||||
// Update filter and grid
|
// Update filter and grid
|
||||||
currentFilterTag = button.dataset.tag;
|
currentFilterTag = button.dataset.tag;
|
||||||
updateGrid();
|
updateGrid();
|
||||||
|
|
||||||
|
// If tag changes but search is active, keep it integrated
|
||||||
|
if (cy && currentFilterTag !== 'all') {
|
||||||
|
// Find the tag node
|
||||||
|
const tagNode = cy.getElementById(`tag-${currentFilterTag}`);
|
||||||
|
if (tagNode.length > 0) {
|
||||||
|
// Center the view on this tag
|
||||||
|
cy.animate({
|
||||||
|
center: { eles: tagNode },
|
||||||
|
zoom: 1.5
|
||||||
|
}, {
|
||||||
|
duration: 500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initial grid population on client side
|
// Initial grid population on client side
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
updateGrid(); // Call after DOM is fully loaded
|
updateGrid(); // Call after DOM is fully loaded
|
||||||
|
|
||||||
|
// Create link between graph and grid
|
||||||
|
document.addEventListener('graphReady', (e) => {
|
||||||
|
// Add a scroll-to-graph button
|
||||||
|
const searchSection = document.querySelector('.search-filter-section');
|
||||||
|
if (searchSection) {
|
||||||
|
const graphButton = document.createElement('button');
|
||||||
|
graphButton.className = 'graph-toggle-btn';
|
||||||
|
graphButton.innerHTML = `
|
||||||
|
<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">
|
||||||
|
<circle cx="11" cy="11" r="8"></circle>
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
|
||||||
|
<line x1="11" y1="8" x2="11" y2="14"></line>
|
||||||
|
<line x1="8" y1="11" x2="14" y2="11"></line>
|
||||||
|
</svg>
|
||||||
|
Explore Knowledge Graph
|
||||||
|
`;
|
||||||
|
graphButton.addEventListener('click', () => {
|
||||||
|
document.querySelector('.knowledge-graph-wrapper').scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
searchSection.appendChild(graphButton);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
@ -407,6 +444,32 @@ const commands = [
|
||||||
background: linear-gradient(180deg, var(--bg-secondary), var(--bg-primary));
|
background: linear-gradient(180deg, var(--bg-secondary), var(--bg-primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 20% 35%, rgba(6, 182, 212, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 75% 15%, rgba(59, 130, 246, 0.1) 0%, transparent 45%),
|
||||||
|
radial-gradient(circle at 85% 70%, rgba(139, 92, 246, 0.1) 0%, transparent 40%);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-bg::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(226, 232, 240, 0.03) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(226, 232, 240, 0.03) 1px, transparent 1px);
|
||||||
|
background-size: 30px 30px;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
@ -465,17 +528,15 @@ const commands = [
|
||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Graph Section */
|
/* Blog Content Section */
|
||||||
.graph-section {
|
.blog-content-section {
|
||||||
padding: 5rem 0;
|
padding: 2rem 0 5rem;
|
||||||
position: relative;
|
|
||||||
background: linear-gradient(0deg, var(--bg-primary), var(--bg-secondary), var(--bg-primary));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
.section-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto 3rem;
|
margin: 0 auto 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
|
@ -503,75 +564,56 @@ const commands = [
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-container {
|
/* Search Filter Container with Knowledge Graph */
|
||||||
position: relative;
|
.search-filter-container {
|
||||||
height: 60vh;
|
margin-bottom: 4rem;
|
||||||
min-height: 500px;
|
background: rgba(15, 23, 42, 0.3);
|
||||||
max-height: 800px;
|
border-radius: 12px;
|
||||||
border-radius: 10px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--card-border);
|
|
||||||
background: rgba(15, 23, 42, 0.2);
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.graph-controls {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.graph-filter {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid var(--border-primary);
|
border: 1px solid var(--border-primary);
|
||||||
background: var(--bg-secondary);
|
overflow: hidden;
|
||||||
color: var(--text-primary);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-filter:hover {
|
.knowledge-graph-wrapper {
|
||||||
border-color: var(--accent-primary);
|
width: 100%;
|
||||||
box-shadow: 0 0 10px var(--glow-primary);
|
padding: 0;
|
||||||
}
|
margin: 0 0 1.5rem;
|
||||||
|
|
||||||
.graph-filter.active {
|
|
||||||
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
|
||||||
border-color: transparent;
|
|
||||||
color: var(--bg-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blog Posts Section */
|
|
||||||
.blog-posts-section {
|
|
||||||
padding: 5rem 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-filter-section {
|
.search-filter-section {
|
||||||
margin-bottom: 3rem;
|
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
background: rgba(13, 21, 41, 0.5);
|
position: relative;
|
||||||
border: 1px solid var(--card-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-bar {
|
.search-bar {
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input {
|
.search-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem 0.75rem 2.5rem;
|
||||||
background-color: var(--bg-primary);
|
background-color: var(--bg-primary);
|
||||||
border: 1px solid var(--card-border);
|
border: 1px solid var(--card-border);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(6, 182, 212, 0.2);
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-input::placeholder {
|
.search-input::placeholder {
|
||||||
|
@ -608,6 +650,7 @@ const commands = [
|
||||||
background-color: rgba(226, 232, 240, 0.1);
|
background-color: rgba(226, 232, 240, 0.1);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
border-color: rgba(56, 189, 248, 0.4);
|
border-color: rgba(56, 189, 248, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-filter-btn.active {
|
.tag-filter-btn.active {
|
||||||
|
@ -616,20 +659,37 @@ const commands = [
|
||||||
border-color: var(--accent-primary);
|
border-color: var(--accent-primary);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles for the integrated graph container */
|
.graph-toggle-btn {
|
||||||
.integrated-graph-container {
|
position: absolute;
|
||||||
margin-top: 2rem; /* Add space above the graph */
|
top: 1.5rem;
|
||||||
height: 400px; /* Adjust height as needed */
|
right: 1.5rem;
|
||||||
border: 1px solid var(--border-primary);
|
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||||
|
color: var(--bg-primary);
|
||||||
|
border: none;
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: rgba(15, 23, 42, 0.3); /* Slightly different background */
|
font-size: 0.9rem;
|
||||||
position: relative; /* Needed for Cytoscape */
|
cursor: pointer;
|
||||||
overflow: hidden; /* Hide scrollbars if graph overflows */
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
box-shadow: 0 4px 10px rgba(6, 182, 212, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-toggle-btn:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 15px rgba(6, 182, 212, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blog Section and Grid */
|
||||||
|
.blog-section {
|
||||||
|
margin-top: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blog-grid {
|
.blog-grid {
|
||||||
margin: 2rem 0 4rem;
|
margin: 2rem 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
@ -661,6 +721,11 @@ const commands = [
|
||||||
border-color: rgba(56, 189, 248, 0.4);
|
border-color: rgba(56, 189, 248, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-image-link {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
.post-image-container {
|
.post-image-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -669,6 +734,11 @@ const commands = [
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:hover .post-image {
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-category-badge {
|
.post-category-badge {
|
||||||
|
@ -717,6 +787,18 @@ const commands = [
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.draft-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: rgba(245, 158, 11, 0.2);
|
||||||
|
color: #F59E0B;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
.post-excerpt {
|
.post-excerpt {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
@ -742,12 +824,19 @@ const commands = [
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-tag {
|
.post-tag {
|
||||||
background: rgba(226, 232, 240, 0.05);
|
background: rgba(16, 185, 129, 0.1);
|
||||||
color: var(--text-secondary);
|
color: #10B981;
|
||||||
padding: 0.2rem 0.5rem;
|
padding: 0.2rem 0.5rem;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-tag:hover {
|
||||||
|
background: rgba(16, 185, 129, 0.2);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.read-more {
|
.read-more {
|
||||||
|
@ -766,6 +855,11 @@ const commands = [
|
||||||
|
|
||||||
.read-more::after {
|
.read-more::after {
|
||||||
content: '→';
|
content: '→';
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:hover .read-more::after {
|
||||||
|
transform: translateX(3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-results {
|
.no-results {
|
||||||
|
@ -815,8 +909,19 @@ const commands = [
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.graph-container {
|
.hero-text {
|
||||||
height: 50vh;
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-description {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-toggle-btn {
|
||||||
|
top: auto;
|
||||||
|
bottom: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -836,5 +941,45 @@ const commands = [
|
||||||
.blog-grid {
|
.blog-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.graph-toggle-btn {
|
||||||
|
padding: 0.5rem;
|
||||||
|
right: 1rem;
|
||||||
|
bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-toggle-btn span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Add CSS to make the image link more obvious on hover */
|
||||||
|
.post-image-link {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px 8px 0 0; /* Match card radius */
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image-link:hover .post-image {
|
||||||
|
transform: scale(1.05);
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image-link::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(6, 182, 212, 0.1); /* Use accent color with alpha */
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
pointer-events: none; /* Allow clicks through */
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image-link:hover::after {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,17 +1,21 @@
|
||||||
// src/pages/search-index.json.js
|
// src/pages/search-index.json.js
|
||||||
// Generates a JSON file with all posts for client-side search
|
// Generates a JSON file with content from all collections for site-wide search
|
||||||
|
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
export async function get() {
|
export async function get() {
|
||||||
// Get all posts
|
// Get content from all collections
|
||||||
const allPosts = await getCollection('posts', ({ data }) => {
|
const posts = await getCollection('posts', ({ data }) => {
|
||||||
// Exclude draft posts in production
|
// Exclude draft posts in production
|
||||||
return import.meta.env.PROD ? !data.draft : true;
|
return import.meta.env.PROD ? !data.draft : true;
|
||||||
});
|
}).catch(() => []);
|
||||||
|
|
||||||
|
const projects = await getCollection('projects').catch(() => []);
|
||||||
|
const configurations = await getCollection('configurations').catch(() => []);
|
||||||
|
const externalPosts = await getCollection('external-posts').catch(() => []);
|
||||||
|
|
||||||
// Transform posts into search-friendly format
|
// Transform posts into search-friendly format
|
||||||
const searchablePosts = allPosts.map(post => ({
|
const searchablePosts = posts.map(post => ({
|
||||||
slug: post.slug,
|
slug: post.slug,
|
||||||
title: post.data.title,
|
title: post.data.title,
|
||||||
description: post.data.description || '',
|
description: post.data.description || '',
|
||||||
|
@ -19,14 +23,60 @@ export async function get() {
|
||||||
category: post.data.category || 'Uncategorized',
|
category: post.data.category || 'Uncategorized',
|
||||||
tags: post.data.tags || [],
|
tags: post.data.tags || [],
|
||||||
readTime: post.data.readTime || '5 min read',
|
readTime: post.data.readTime || '5 min read',
|
||||||
|
type: 'post',
|
||||||
|
url: `/posts/${post.slug}/`
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Transform projects
|
||||||
|
const searchableProjects = projects.map(project => ({
|
||||||
|
slug: project.slug,
|
||||||
|
title: project.data.title,
|
||||||
|
description: project.data.description || '',
|
||||||
|
pubDate: project.data.pubDate ? new Date(project.data.pubDate).toISOString() : '',
|
||||||
|
category: project.data.category || 'Projects',
|
||||||
|
tags: project.data.tags || [],
|
||||||
|
type: 'project',
|
||||||
|
url: `/projects/${project.slug}/`
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Transform configurations
|
||||||
|
const searchableConfigurations = configurations.map(config => ({
|
||||||
|
slug: config.slug,
|
||||||
|
title: config.data.title,
|
||||||
|
description: config.data.description || '',
|
||||||
|
pubDate: config.data.pubDate ? new Date(config.data.pubDate).toISOString() : '',
|
||||||
|
category: config.data.category || 'Configurations',
|
||||||
|
tags: config.data.tags || [],
|
||||||
|
type: 'configuration',
|
||||||
|
url: `/configurations/${config.slug}/`
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Transform external posts
|
||||||
|
const searchableExternalPosts = externalPosts.map(post => ({
|
||||||
|
slug: post.slug,
|
||||||
|
title: post.data.title,
|
||||||
|
description: post.data.description || '',
|
||||||
|
pubDate: post.data.pubDate ? new Date(post.data.pubDate).toISOString() : '',
|
||||||
|
category: post.data.category || 'External',
|
||||||
|
tags: post.data.tags || [],
|
||||||
|
type: 'external',
|
||||||
|
url: post.data.url // Use the external URL directly
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Combine all searchable content
|
||||||
|
const allSearchableContent = [
|
||||||
|
...searchablePosts,
|
||||||
|
...searchableProjects,
|
||||||
|
...searchableConfigurations,
|
||||||
|
...searchableExternalPosts
|
||||||
|
];
|
||||||
|
|
||||||
// Return JSON
|
// Return JSON
|
||||||
return {
|
return {
|
||||||
body: JSON.stringify(searchablePosts),
|
body: JSON.stringify(allSearchableContent),
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Cache-Control': 'max-age=3600'
|
'Cache-Control': 'max-age=3600'
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
|
@ -1,265 +1,483 @@
|
||||||
---
|
---
|
||||||
// src/pages/tag/[tag].astro
|
// src/pages/tag/[tag].astro
|
||||||
// Dynamic route for tag pages
|
// Dynamic route for tag pages with enhanced visualization
|
||||||
|
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
import Header from '../../components/Header.astro';
|
||||||
|
import Footer from '../../components/Footer.astro';
|
||||||
|
import KnowledgeGraph from '../../components/KnowledgeGraph.astro';
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const allPosts = await getCollection('blog');
|
try {
|
||||||
const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())];
|
// Get all posts
|
||||||
|
const allPosts = await getCollection('posts', ({ data }) => {
|
||||||
return uniqueTags.map((tag) => {
|
// Exclude draft posts in production
|
||||||
const filteredPosts = allPosts.filter((post) => post.data.tags.includes(tag));
|
return import.meta.env.PROD ? !data.draft : true;
|
||||||
return {
|
});
|
||||||
params: { tag },
|
|
||||||
props: { posts: filteredPosts },
|
// Extract all unique tags
|
||||||
};
|
const allTags = [...new Set(allPosts.flatMap(post => post.data.tags || []))];
|
||||||
});
|
|
||||||
|
// Create a path for each tag
|
||||||
|
return allTags.map((tag) => {
|
||||||
|
// Filter posts to only those with this tag
|
||||||
|
const filteredPosts = allPosts.filter((post) =>
|
||||||
|
(post.data.tags || []).includes(tag)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
params: { tag },
|
||||||
|
props: {
|
||||||
|
posts: filteredPosts,
|
||||||
|
tag,
|
||||||
|
allPosts // Pass all posts for knowledge graph
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error in getStaticPaths:", error);
|
||||||
|
// Return empty array as fallback
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { tag } = Astro.params;
|
const { tag } = Astro.params;
|
||||||
const { posts } = Astro.props;
|
const { posts, allPosts } = Astro.props;
|
||||||
|
|
||||||
// Format date
|
// Format dates
|
||||||
const formatDate = (dateStr) => {
|
const formatDate = (dateStr) => {
|
||||||
|
if (!dateStr) return '';
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
|
return date.toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sort posts by date (newest first)
|
// Sort posts by date (newest first)
|
||||||
const sortedPosts = posts.sort((a, b) => {
|
const sortedPosts = [...posts].sort((a, b) => {
|
||||||
const dateA = new Date(a.data.pubDate);
|
const dateA = new Date(a.data.pubDate || 0);
|
||||||
const dateB = new Date(b.data.pubDate);
|
const dateB = new Date(b.data.pubDate || 0);
|
||||||
return dateB.getTime() - dateA.getTime();
|
return dateB.getTime() - dateA.getTime();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Prepare Knowledge Graph data
|
||||||
|
const graphData = {
|
||||||
|
nodes: [
|
||||||
|
// Add the current tag as a central node
|
||||||
|
{
|
||||||
|
id: `tag-${tag}`,
|
||||||
|
label: tag,
|
||||||
|
type: 'tag',
|
||||||
|
url: `/tag/${tag}`
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add posts with this tag
|
||||||
|
...sortedPosts.map(post => ({
|
||||||
|
id: post.slug,
|
||||||
|
label: post.data.title,
|
||||||
|
type: 'post',
|
||||||
|
category: post.data.category || 'Uncategorized',
|
||||||
|
tags: post.data.tags || [],
|
||||||
|
url: `/posts/${post.slug}/`
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Add related tags (tags that appear alongside this tag in posts)
|
||||||
|
...posts.flatMap(post =>
|
||||||
|
(post.data.tags || [])
|
||||||
|
.filter(t => t !== tag) // Don't include current tag
|
||||||
|
.map(relatedTag => ({
|
||||||
|
id: `tag-${relatedTag}`,
|
||||||
|
label: relatedTag,
|
||||||
|
type: 'tag',
|
||||||
|
url: `/tag/${relatedTag}`
|
||||||
|
}))
|
||||||
|
).filter((v, i, a) => a.findIndex(t => t.id === v.id) === i) // Deduplicate
|
||||||
|
],
|
||||||
|
edges: [
|
||||||
|
// Connect posts to the current tag
|
||||||
|
...sortedPosts.map(post => ({
|
||||||
|
source: post.slug,
|
||||||
|
target: `tag-${tag}`,
|
||||||
|
type: 'post-tag',
|
||||||
|
strength: 2
|
||||||
|
})),
|
||||||
|
|
||||||
|
// Connect related tags to their posts
|
||||||
|
...posts.flatMap(post =>
|
||||||
|
(post.data.tags || [])
|
||||||
|
.filter(t => t !== tag) // Skip current tag
|
||||||
|
.map(relatedTag => ({
|
||||||
|
source: post.slug,
|
||||||
|
target: `tag-${relatedTag}`,
|
||||||
|
type: 'post-tag',
|
||||||
|
strength: 1
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
]
|
||||||
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={`Posts tagged with "${tag}" | LaForce IT Blog`} description={`Articles and guides related to ${tag}`}>
|
<BaseLayout title={`Posts tagged with "${tag}" | LaForce IT Blog`} description={`Articles and guides related to ${tag}`}>
|
||||||
<div class="container tag-page">
|
<Header slot="header" />
|
||||||
<header class="tag-hero">
|
|
||||||
<h1>Posts tagged with <span class="tag-highlight">{tag}</span></h1>
|
<main class="tag-page-container">
|
||||||
<p>Explore {sortedPosts.length} {sortedPosts.length === 1 ? 'article' : 'articles'} related to {tag}</p>
|
<section class="tag-hero">
|
||||||
</header>
|
<div class="container">
|
||||||
|
<h1>Posts tagged with <span class="tag-highlight">{tag}</span></h1>
|
||||||
<div class="posts-grid">
|
<p class="tag-description">
|
||||||
{sortedPosts.map((post) => (
|
Explore {sortedPosts.length} {sortedPosts.length === 1 ? 'article' : 'articles'} related to {tag}
|
||||||
<article class="post-card">
|
</p>
|
||||||
<!-- Simplified image rendering that works reliably -->
|
</div>
|
||||||
<img
|
</section>
|
||||||
width={720}
|
|
||||||
height={360}
|
<section class="tag-content container">
|
||||||
src={post.data.heroImage || "/images/placeholders/default.jpg"}
|
<div class="knowledge-graph-section">
|
||||||
alt=""
|
<h2>Content Connections</h2>
|
||||||
class="post-image"
|
<p class="section-description">
|
||||||
/>
|
Explore how {tag} relates to other content and tags
|
||||||
<div class="post-content">
|
</p>
|
||||||
<time datetime={post.data.pubDate}>{formatDate(post.data.pubDate)}</time>
|
|
||||||
<h2 class="post-title">
|
<div class="graph-wrapper">
|
||||||
<a href={`/posts/${post.slug}/`}>{post.data.title}</a>
|
<KnowledgeGraph graphData={graphData} height="400px" initialFilter="all" />
|
||||||
</h2>
|
</div>
|
||||||
<p class="post-excerpt">{post.data.description}</p>
|
</div>
|
||||||
<div class="post-meta">
|
|
||||||
<span class="reading-time">{post.data.minutesRead || '5 min'} read</span>
|
<div class="posts-section">
|
||||||
<ul class="post-tags">
|
<h2>Articles</h2>
|
||||||
{post.data.tags.map((tagName) => (
|
<div class="posts-grid">
|
||||||
<li>
|
{sortedPosts.length > 0 ? sortedPosts.map((post) => (
|
||||||
<a href={`/tag/${tagName}`} class={tagName === tag ? 'current-tag' : ''}>
|
<article class="post-card">
|
||||||
{tagName}
|
<a href={`/posts/${post.slug}/`} class="post-card-link">
|
||||||
</a>
|
{post.data.heroImage && (
|
||||||
</li>
|
<div class="post-image-wrapper">
|
||||||
))}
|
<img
|
||||||
</ul>
|
src={post.data.heroImage}
|
||||||
|
alt=""
|
||||||
|
class="post-image"
|
||||||
|
width="400"
|
||||||
|
height="225"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="post-content">
|
||||||
|
<div class="post-meta">
|
||||||
|
<time datetime={post.data.pubDate?.toISOString()}>
|
||||||
|
{formatDate(post.data.pubDate)}
|
||||||
|
</time>
|
||||||
|
{post.data.readTime && (
|
||||||
|
<span class="read-time">{post.data.readTime}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="post-title">{post.data.title}</h3>
|
||||||
|
|
||||||
|
{post.data.description && (
|
||||||
|
<p class="post-description">{post.data.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{post.data.tags && post.data.tags.length > 0 && (
|
||||||
|
<div class="post-tags">
|
||||||
|
{post.data.tags.map(postTag => (
|
||||||
|
<span class={`post-tag ${postTag === tag ? 'current-tag' : ''}`}>
|
||||||
|
#{postTag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</article>
|
||||||
|
)) : (
|
||||||
|
<div class="no-posts">
|
||||||
|
<p>No posts found with the tag "{tag}".</p>
|
||||||
|
<a href="/blog" class="back-to-blog">Browse all posts</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</article>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="tag-navigation">
|
||||||
<a href="/tags" class="all-tags-link">
|
<a href="/blog" class="back-button">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<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">
|
||||||
<line x1="19" y1="12" x2="5" y2="12"></line>
|
<line x1="19" y1="12" x2="5" y2="12"></line>
|
||||||
<polyline points="12 19 5 12 12 5"></polyline>
|
<polyline points="12 19 5 12 12 5"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
View all tags
|
Back to All Posts
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer slot="footer" />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.tag-page {
|
.tag-page-container {
|
||||||
padding-top: 2rem;
|
|
||||||
padding-bottom: 4rem;
|
padding-bottom: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-hero {
|
.container {
|
||||||
text-align: center;
|
max-width: 1280px;
|
||||||
margin-bottom: 3rem;
|
margin: 0 auto;
|
||||||
animation: fadeIn 0.5s ease-out;
|
padding: 0 var(--container-padding, 1.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-hero {
|
||||||
|
padding: 5rem 0 3rem;
|
||||||
|
background: linear-gradient(to bottom, var(--bg-secondary), var(--bg-primary));
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-hero::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: radial-gradient(circle at 30% 50%, rgba(6, 182, 212, 0.05) 0%, transparent 50%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-hero h1 {
|
||||||
|
font-size: clamp(1.8rem, 4vw, 3rem);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
animation: fadeInUp 0.6s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-highlight {
|
||||||
|
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-description {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: clamp(1rem, 2vw, 1.2rem);
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
animation: fadeInUp 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-content {
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-graph-section,
|
||||||
|
.posts-section {
|
||||||
|
margin-bottom: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.knowledge-graph-section h2,
|
||||||
|
.posts-section h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-description {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.graph-wrapper {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.posts-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card {
|
||||||
|
height: 100%;
|
||||||
|
animation: fadeIn 0.6s ease-out forwards;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:nth-child(1) { animation-delay: 0.1s; }
|
||||||
|
.post-card:nth-child(2) { animation-delay: 0.2s; }
|
||||||
|
.post-card:nth-child(3) { animation-delay: 0.3s; }
|
||||||
|
.post-card:nth-child(4) { animation-delay: 0.4s; }
|
||||||
|
.post-card:nth-child(5) { animation-delay: 0.5s; }
|
||||||
|
.post-card:nth-child(6) { animation-delay: 0.6s; }
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; transform: translateY(10px); }
|
from { opacity: 0; transform: translateY(10px); }
|
||||||
to { opacity: 1; transform: translateY(0); }
|
to { opacity: 1; transform: translateY(0); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-hero h1 {
|
.post-card-link {
|
||||||
font-size: var(--font-size-3xl);
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-highlight {
|
|
||||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tag-hero p {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: var(--font-size-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.posts-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-card {
|
|
||||||
background: var(--card-bg);
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid var(--border-primary);
|
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
||||||
animation: fadeIn 0.5s ease-out forwards;
|
|
||||||
animation-delay: calc(var(--animation-order, 0) * 0.1s);
|
|
||||||
opacity: 0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--card-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-card:hover {
|
.post-card-link:hover {
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||||
border-color: var(--accent-primary);
|
border-color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-image-wrapper {
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-bottom: 1px solid var(--card-border);
|
||||||
|
}
|
||||||
|
|
||||||
.post-image {
|
.post-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 200px;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-bottom: 1px solid var(--border-primary);
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card-link:hover .post-image {
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-content {
|
.post-content {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.post-content time {
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-title {
|
|
||||||
font-size: var(--font-size-xl);
|
|
||||||
margin: 0.5rem 0 1rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-title a {
|
|
||||||
color: var(--text-primary);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-title a:hover {
|
|
||||||
color: var(--accent-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-excerpt {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: var(--font-size-md);
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-meta {
|
.post-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: space-between;
|
||||||
gap: 0.75rem;
|
font-size: 0.85rem;
|
||||||
margin-top: auto;
|
color: var(--text-tertiary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reading-time {
|
.read-time {
|
||||||
color: var(--text-tertiary);
|
|
||||||
font-size: var(--font-size-sm);
|
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-description {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.post-tags {
|
.post-tags {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
list-style: none;
|
margin-top: auto;
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-tags li a {
|
.post-tag {
|
||||||
display: block;
|
font-size: 0.75rem;
|
||||||
padding: 0.25rem 0.75rem;
|
color: var(--text-secondary);
|
||||||
background: rgba(56, 189, 248, 0.1);
|
background: rgba(226, 232, 240, 0.05);
|
||||||
border-radius: 20px;
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-tag.current-tag {
|
||||||
|
background: rgba(16, 185, 129, 0.2);
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-tags li a:hover {
|
.no-posts {
|
||||||
background: rgba(56, 189, 248, 0.2);
|
grid-column: 1 / -1;
|
||||||
transform: translateY(-2px);
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px dashed var(--card-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-tags li a.current-tag {
|
.back-to-blog {
|
||||||
background: var(--accent-primary);
|
display: inline-block;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||||
color: var(--bg-primary);
|
color: var(--bg-primary);
|
||||||
|
border-radius: 6px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.all-tags-link {
|
.back-to-blog:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 5px 15px rgba(6, 182, 212, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin: 0 auto;
|
padding: 0.8rem 1.5rem;
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-primary);
|
border: 1px solid var(--border-primary);
|
||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: var(--font-size-md);
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.3s ease;
|
||||||
width: fit-content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.all-tags-link:hover {
|
.back-button:hover {
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.tag-hero h1 {
|
.tag-hero {
|
||||||
font-size: var(--font-size-2xl);
|
padding: 4rem 0 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.posts-grid {
|
.posts-grid {
|
||||||
|
|
|
@ -0,0 +1,179 @@
|
||||||
|
---
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import Header from '../components/Header.astro';
|
||||||
|
import Footer from '../components/Footer.astro';
|
||||||
|
import MiniKnowledgeGraph from '../components/MiniKnowledgeGraph.astro';
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
|
||||||
|
// Get all posts
|
||||||
|
const allPosts = await getCollection('posts').catch(error => {
|
||||||
|
console.error('Error fetching posts collection:', error);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Try blog collection if posts doesn't exist
|
||||||
|
const blogPosts = allPosts.length === 0 ? await getCollection('blog').catch(() => []) : [];
|
||||||
|
const combinedPosts = [...allPosts, ...blogPosts];
|
||||||
|
|
||||||
|
// Use the first post as a test post
|
||||||
|
const testPost = combinedPosts.length > 0 ? combinedPosts[0] : {
|
||||||
|
slug: 'test-post',
|
||||||
|
data: {
|
||||||
|
title: 'Test Post',
|
||||||
|
tags: ['test', 'graph'],
|
||||||
|
category: 'Test'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create related posts - use the next 3 posts in the collection or create test posts
|
||||||
|
const relatedPosts = combinedPosts.length > 1
|
||||||
|
? combinedPosts.slice(1, 4)
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
slug: 'related-1',
|
||||||
|
data: {
|
||||||
|
title: 'Related Post 1',
|
||||||
|
tags: ['test', 'graph'],
|
||||||
|
category: 'Test'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'related-2',
|
||||||
|
data: {
|
||||||
|
title: 'Related Post 2',
|
||||||
|
tags: ['test'],
|
||||||
|
category: 'Test'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title="Test MiniKnowledgeGraph">
|
||||||
|
<Header slot="header" />
|
||||||
|
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<h1 class="text-3xl font-bold mb-6">MiniKnowledgeGraph Test Page</h1>
|
||||||
|
|
||||||
|
<div class="bg-slate-800 rounded-lg p-6 mb-8">
|
||||||
|
<p class="mb-4">This is a test page to ensure the MiniKnowledgeGraph component is working properly.</p>
|
||||||
|
|
||||||
|
<div class="border border-slate-700 rounded-lg p-4 mb-6">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Test Post Details:</h2>
|
||||||
|
<p><strong>Title:</strong> {testPost.data.title}</p>
|
||||||
|
<p><strong>Slug:</strong> {testPost.slug}</p>
|
||||||
|
<p><strong>Tags:</strong> {testPost.data.tags?.join(', ') || 'None'}</p>
|
||||||
|
<p><strong>Category:</strong> {testPost.data.category || 'None'}</p>
|
||||||
|
<p><strong>Related Posts:</strong> {relatedPosts.length}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mini-knowledge-graph-area">
|
||||||
|
<h2 class="text-xl font-bold mb-4">MiniKnowledgeGraph Component:</h2>
|
||||||
|
|
||||||
|
<div class="mini-knowledge-graph-wrapper">
|
||||||
|
<MiniKnowledgeGraph
|
||||||
|
currentPost={testPost}
|
||||||
|
relatedPosts={relatedPosts}
|
||||||
|
height="300px"
|
||||||
|
title="Test Graph"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Debug Information Display -->
|
||||||
|
<div class="debug-info mt-4 p-4 bg-gray-900 rounded-lg">
|
||||||
|
<h3 class="text-lg font-bold mb-2">Debug Info</h3>
|
||||||
|
<div id="debug-container">Loading debug info...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer slot="footer" />
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mini-knowledge-graph-area {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mini-knowledge-graph-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: block !important;
|
||||||
|
position: relative;
|
||||||
|
min-height: 300px;
|
||||||
|
height: 300px;
|
||||||
|
background: var(--card-bg, #1e293b);
|
||||||
|
border: 1px solid var(--card-border, #334155);
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug-info {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Debug utility for testing the knowledge graph
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
setTimeout(checkGraphStatus, 500);
|
||||||
|
|
||||||
|
// Also check after window load
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
setTimeout(checkGraphStatus, 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function checkGraphStatus() {
|
||||||
|
const debugContainer = document.getElementById('debug-container');
|
||||||
|
if (!debugContainer) return;
|
||||||
|
|
||||||
|
// Get container info
|
||||||
|
const container = document.querySelector('.mini-knowledge-graph-wrapper');
|
||||||
|
const cyContainer = document.getElementById('mini-cy');
|
||||||
|
|
||||||
|
// Check for cytoscape instance
|
||||||
|
const cyInstance = window.miniCy;
|
||||||
|
|
||||||
|
let html = '<ul>';
|
||||||
|
|
||||||
|
// Container dimensions
|
||||||
|
if (container) {
|
||||||
|
html += `<li>Container: ${container.offsetWidth}x${container.offsetHeight}px</li>`;
|
||||||
|
html += `<li>Display: ${getComputedStyle(container).display}</li>`;
|
||||||
|
html += `<li>Visibility: ${getComputedStyle(container).visibility}</li>`;
|
||||||
|
} else {
|
||||||
|
html += '<li>Container: Not found</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cytoscape container
|
||||||
|
if (cyContainer) {
|
||||||
|
html += `<li>Cy Container: ${cyContainer.offsetWidth}x${cyContainer.offsetHeight}px</li>`;
|
||||||
|
} else {
|
||||||
|
html += '<li>Cy Container: Not found</li>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cytoscape instance
|
||||||
|
html += `<li>Cytoscape object: ${typeof cytoscape !== 'undefined' ? 'Available' : 'Not available'}</li>`;
|
||||||
|
html += `<li>Cytoscape instance: ${cyInstance ? 'Initialized' : 'Not initialized'}</li>`;
|
||||||
|
|
||||||
|
// If instance exists, get more details
|
||||||
|
if (cyInstance) {
|
||||||
|
html += `<li>Nodes: ${cyInstance.nodes().length}</li>`;
|
||||||
|
html += `<li>Edges: ${cyInstance.edges().length}</li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</ul>';
|
||||||
|
|
||||||
|
// Add refresh button
|
||||||
|
html += '<button id="refresh-debug" class="mt-2 px-3 py-1 bg-blue-700 text-white rounded hover:bg-blue-600">' +
|
||||||
|
'Refresh Debug Info</button>';
|
||||||
|
|
||||||
|
debugContainer.innerHTML = html;
|
||||||
|
|
||||||
|
// Add event listener to refresh button
|
||||||
|
document.getElementById('refresh-debug')?.addEventListener('click', checkGraphStatus);
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -37,12 +37,12 @@
|
||||||
--bg-secondary-rgb: 22, 26, 36; /* RGB for gradients */
|
--bg-secondary-rgb: 22, 26, 36; /* RGB for gradients */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light Mode Variables */
|
/* Enhanced Light Mode Variables - More tech-focused, less plain white */
|
||||||
:root.light-mode {
|
:root.light-mode {
|
||||||
--bg-primary: #ffffff;
|
--bg-primary: #f0f4f8; /* Subtle blue-gray instead of white */
|
||||||
--bg-secondary: #f8fafc; /* Lighter secondary */
|
--bg-secondary: #e5eaf2; /* Slightly darker secondary */
|
||||||
--bg-tertiary: #f1f5f9; /* Even lighter tertiary */
|
--bg-tertiary: #dae2ef; /* Even more blue tint for tertiary */
|
||||||
--bg-code: #f1f5f9;
|
--bg-code: #e5edf7;
|
||||||
--text-primary: #1e293b; /* Darker primary text */
|
--text-primary: #1e293b; /* Darker primary text */
|
||||||
--text-secondary: #475569; /* Darker secondary text */
|
--text-secondary: #475569; /* Darker secondary text */
|
||||||
--text-tertiary: #64748b; /* Darker tertiary text */
|
--text-tertiary: #64748b; /* Darker tertiary text */
|
||||||
|
@ -52,14 +52,14 @@
|
||||||
--glow-primary: rgba(8, 145, 178, 0.15);
|
--glow-primary: rgba(8, 145, 178, 0.15);
|
||||||
--glow-secondary: rgba(37, 99, 235, 0.15);
|
--glow-secondary: rgba(37, 99, 235, 0.15);
|
||||||
--glow-tertiary: rgba(124, 58, 237, 0.15);
|
--glow-tertiary: rgba(124, 58, 237, 0.15);
|
||||||
--border-primary: rgba(0, 0, 0, 0.1); /* Darker borders */
|
--border-primary: rgba(37, 99, 235, 0.15); /* More visible blue-tinted borders */
|
||||||
--border-secondary: rgba(0, 0, 0, 0.05);
|
--border-secondary: rgba(8, 145, 178, 0.1);
|
||||||
--card-bg: rgba(255, 255, 255, 0.8); /* White card with opacity */
|
--card-bg: rgba(255, 255, 255, 0.6); /* More transparent card background */
|
||||||
--card-border: rgba(37, 99, 235, 0.3); /* Blue border */
|
--card-border: rgba(37, 99, 235, 0.2); /* Subtle blue border */
|
||||||
--ui-element: #e2e8f0; /* Lighter UI elements */
|
--ui-element: rgba(226, 232, 240, 0.7); /* More transparent UI elements */
|
||||||
--ui-element-hover: #cbd5e1;
|
--ui-element-hover: rgba(203, 213, 225, 0.8);
|
||||||
--bg-primary-rgb: 255, 255, 255; /* RGB for gradients */
|
--bg-primary-rgb: 240, 244, 248; /* RGB for gradients */
|
||||||
--bg-secondary-rgb: 248, 250, 252; /* RGB for gradients */
|
--bg-secondary-rgb: 229, 234, 242; /* RGB for gradients */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure transitions for smooth theme changes */
|
/* Ensure transitions for smooth theme changes */
|
||||||
|
@ -67,24 +67,40 @@
|
||||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Knowledge Graph specific theme adjustments */
|
/* Knowledge Graph specific theme adjustments - More transparent in light mode */
|
||||||
:root.light-mode .graph-container {
|
:root.light-mode .graph-container {
|
||||||
background: rgba(248, 250, 252, 0.3);
|
background: rgba(248, 250, 252, 0.08); /* Much more transparent - lighter gray */
|
||||||
border: 1px solid var(--card-border);
|
backdrop-filter: blur(2px);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.15);
|
||||||
|
box-shadow: 0 4px 20px rgba(37, 99, 235, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.light-mode .node-details {
|
:root.light-mode .node-details {
|
||||||
background: rgba(255, 255, 255, 0.8);
|
background: rgba(255, 255, 255, 0.8); /* More opaque for readability */
|
||||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
backdrop-filter: blur(5px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.light-mode .graph-filters {
|
:root.light-mode .graph-filters {
|
||||||
background: rgba(248, 250, 252, 0.7);
|
background: rgba(248, 250, 252, 0.6); /* Slightly more opaque */
|
||||||
|
backdrop-filter: blur(3px);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.light-mode .graph-filter {
|
:root.light-mode .graph-filter {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
border-color: var(--border-primary);
|
border-color: var(--border-primary);
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .graph-filter:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .graph-filter.active {
|
||||||
|
background: linear-gradient(135deg, rgba(8, 145, 178, 0.1), rgba(37, 99, 235, 0.1));
|
||||||
|
border-color: rgba(37, 99, 235, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.light-mode .connections-list a {
|
:root.light-mode .connections-list a {
|
||||||
|
@ -92,7 +108,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
:root.light-mode .node-link {
|
:root.light-mode .node-link {
|
||||||
box-shadow: 0 4px 10px rgba(8, 145, 178, 0.15);
|
box-shadow: 0 4px 10px rgba(8, 145, 178, 0.1);
|
||||||
|
background: linear-gradient(135deg, rgba(8, 145, 178, 0.1), rgba(37, 99, 235, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .node-link:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(8, 145, 178, 0.2), rgba(37, 99, 235, 0.2));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix for code blocks in light mode */
|
/* Fix for code blocks in light mode */
|
||||||
|
@ -100,6 +121,39 @@
|
||||||
:root.light-mode code {
|
:root.light-mode code {
|
||||||
background-color: var(--bg-code);
|
background-color: var(--bg-code);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Services and Newsletter sections - More transparent in light mode */
|
||||||
|
:root.light-mode .service-card {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .newsletter-container,
|
||||||
|
:root.light-mode .cta-container {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .newsletter-input {
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enhanced light mode body background with more pronounced grid pattern */
|
||||||
|
:root.light-mode body {
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 20% 35%, rgba(8, 145, 178, 0.08) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 75% 15%, rgba(37, 99, 235, 0.08) 0%, transparent 45%),
|
||||||
|
radial-gradient(circle at 85% 70%, rgba(124, 58, 237, 0.08) 0%, transparent 40%),
|
||||||
|
linear-gradient(rgba(37, 99, 235, 0.05) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(37, 99, 235, 0.05) 1px, transparent 1px);
|
||||||
|
background-size: auto, auto, auto, 16px 16px, 16px 16px;
|
||||||
|
background-position: 0 0, 0 0, 0 0, center center, center center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply base styles using variables */
|
/* Apply base styles using variables */
|
||||||
|
@ -129,4 +183,44 @@ input, select, textarea {
|
||||||
.post-card, .sidebar-block {
|
.post-card, .sidebar-block {
|
||||||
background-color: var(--card-bg);
|
background-color: var(--card-bg);
|
||||||
border-color: var(--card-border);
|
border-color: var(--card-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light mode buttons are more attractive */
|
||||||
|
:root.light-mode button {
|
||||||
|
background: linear-gradient(135deg, rgba(8, 145, 178, 0.05), rgba(37, 99, 235, 0.05));
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode button:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(8, 145, 178, 0.1), rgba(37, 99, 235, 0.1));
|
||||||
|
border-color: rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Other light mode improvements */
|
||||||
|
:root.light-mode .primary-button,
|
||||||
|
:root.light-mode .cta-primary-button {
|
||||||
|
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||||
|
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .secondary-button,
|
||||||
|
:root.light-mode .cta-secondary-button {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
border: 1px solid rgba(37, 99, 235, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .hero-section {
|
||||||
|
background: linear-gradient(to bottom, var(--bg-secondary), var(--bg-primary));
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-mode .hero-bg {
|
||||||
|
background-image:
|
||||||
|
radial-gradient(circle at 20% 35%, rgba(8, 145, 178, 0.1) 0%, transparent 50%),
|
||||||
|
radial-gradient(circle at 75% 15%, rgba(37, 99, 235, 0.1) 0%, transparent 45%),
|
||||||
|
radial-gradient(circle at 85% 70%, rgba(124, 58, 237, 0.1) 0%, transparent 40%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for knowledge graph in both themes */
|
||||||
|
.graph-container {
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
}
|
}
|
Loading…
Reference in New Issue