Merge pull request 'fresh-main' (#7) from fresh-main into main
Reviewed-on: https://gitea.argobox.com/KeyArgo/laforceit-blog/pulls/7
This commit is contained in:
commit
ccae3a8c86
|
@ -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
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
|
@ -9,11 +9,11 @@ const categories = [
|
|||
{
|
||||
title: 'Technology',
|
||||
links: [
|
||||
{ name: 'Kubernetes', path: '/blog/category/kubernetes' },
|
||||
{ name: 'Docker', path: '/blog/category/docker' },
|
||||
{ name: 'DevOps', path: '/blog/category/devops' },
|
||||
{ name: 'Networking', path: '/blog/category/networking' },
|
||||
{ name: 'Storage', path: '/blog/category/storage' }
|
||||
{ name: 'Kubernetes', path: '/categories/kubernetes' },
|
||||
{ name: 'Docker', path: '/categories/docker' },
|
||||
{ name: 'DevOps', path: '/categories/devops' },
|
||||
{ name: 'Networking', path: '/categories/networking' },
|
||||
{ name: 'Storage', path: '/categories/storage' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -29,8 +29,8 @@ const categories = [
|
|||
{
|
||||
title: 'Projects',
|
||||
links: [
|
||||
{ name: 'HomeLab Setup', path: '/projects/homelab' },
|
||||
{ name: 'Tech Stack', path: '/projects/tech-stack' },
|
||||
{ name: 'HomeLab Setup', url: 'https://argobox.com' },
|
||||
{ name: 'Tech Stack', url: 'https://argobox.com/#services' },
|
||||
{ name: 'Github Repos', path: '/projects/github' },
|
||||
{ name: 'Live Services', path: '/projects/services' },
|
||||
{ name: 'Obsidian Templates', path: '/projects/obsidian' }
|
||||
|
@ -42,7 +42,7 @@ const categories = [
|
|||
const socialLinks = [
|
||||
{
|
||||
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" />'
|
||||
},
|
||||
{
|
||||
|
@ -130,7 +130,14 @@ const services = [
|
|||
<ul class="footer-links">
|
||||
{category.links.map(link => (
|
||||
<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>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
@ -423,8 +423,8 @@ const currentPath = Astro.url.pathname;
|
|||
}
|
||||
});
|
||||
|
||||
// Search functionality - client-side post filtering
|
||||
const searchResults = document.getElementById('search-results');
|
||||
// Search functionality - client-side site-wide filtering (User provided version)
|
||||
const searchResults = document.getElementById('search-results'); // Assuming this ID exists in your dropdown HTML
|
||||
|
||||
// Function to perform search
|
||||
const performSearch = async (query) => {
|
||||
|
@ -437,39 +437,68 @@ const currentPath = Astro.url.pathname;
|
|||
}
|
||||
|
||||
try {
|
||||
// This would ideally be a server-side search or a pre-built index
|
||||
// For now, we'll just fetch all posts and filter client-side
|
||||
const response = await fetch('/search-index.json');
|
||||
// Fetch the search index that contains all site content
|
||||
const response = await fetch('/search-index.json'); // Ensure this path is correct based on your build output
|
||||
if (!response.ok) throw new Error('Failed to fetch search data');
|
||||
|
||||
const posts = await response.json();
|
||||
const results = posts.filter(post => {
|
||||
const allContent = await response.json();
|
||||
const results = allContent.filter(item => {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
return (
|
||||
post.title.toLowerCase().includes(lowerQuery) ||
|
||||
post.description?.toLowerCase().includes(lowerQuery) ||
|
||||
post.tags?.some(tag => tag.toLowerCase().includes(lowerQuery))
|
||||
item.title.toLowerCase().includes(lowerQuery) ||
|
||||
item.description?.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
|
||||
if (searchResults) {
|
||||
if (results.length > 0) {
|
||||
searchResults.innerHTML = results.map(post => `
|
||||
<div class="search-result-item" data-url="/posts/${post.slug}/">
|
||||
<div class="search-result-title">${post.title}</div>
|
||||
<div class="search-result-snippet">${post.description || ''}</div>
|
||||
searchResults.innerHTML = results.map(item => {
|
||||
// Create type badge
|
||||
let typeBadge = '';
|
||||
switch(item.type) {
|
||||
case 'post':
|
||||
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>
|
||||
`).join('');
|
||||
<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
|
||||
document.querySelectorAll('.search-result-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
window.location.href = item.dataset.url;
|
||||
window.location.href = item.dataset.url; // Navigate to the item's URL
|
||||
});
|
||||
});
|
||||
} 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) {
|
||||
|
@ -480,7 +509,7 @@ const currentPath = Astro.url.pathname;
|
|||
}
|
||||
};
|
||||
|
||||
// Search input event handler
|
||||
// Search input event handler with debounce
|
||||
let searchTimeout;
|
||||
searchInput?.addEventListener('input', (e) => {
|
||||
clearTimeout(searchTimeout);
|
||||
|
@ -489,17 +518,18 @@ const currentPath = Astro.url.pathname;
|
|||
}, 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');
|
||||
searchForm?.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
e.preventDefault(); // Prevent default form submission
|
||||
performSearch(searchInput.value);
|
||||
});
|
||||
|
||||
// Handle search-submit button click
|
||||
const searchSubmit = document.querySelector('.search-submit');
|
||||
// Handle search-submit button click (if you have a separate submit button)
|
||||
const searchSubmit = document.querySelector('.search-submit'); // Adjust selector if needed
|
||||
searchSubmit?.addEventListener('click', () => {
|
||||
performSearch(searchInput?.value || '');
|
||||
});
|
||||
});
|
||||
|
||||
}); // End of DOMContentLoaded
|
||||
</script>
|
|
@ -8,6 +8,8 @@ import Footer from '../components/Footer.astro';
|
|||
import Terminal from '../components/Terminal.astro';
|
||||
import KnowledgeGraph from '../components/KnowledgeGraph.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { Image } from 'astro:assets';
|
||||
import { COMMON_COMMANDS, TERMINAL_CONTENT } from '../config/terminal.js';
|
||||
|
||||
// Get all blog entries
|
||||
const allPosts = await getCollection('posts');
|
||||
|
@ -22,38 +24,8 @@ const sortedPosts = allPosts.sort((a, b) => {
|
|||
// Get recent posts (latest 4)
|
||||
const recentPosts = sortedPosts.slice(0, 4);
|
||||
|
||||
// Prepare terminal commands
|
||||
const terminalCommands = [
|
||||
{
|
||||
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 terminal commands - now imported from central config
|
||||
const terminalCommands = COMMON_COMMANDS;
|
||||
|
||||
// Prepare graph data for knowledge map
|
||||
// Extract categories and tags from posts
|
||||
|
@ -364,6 +336,38 @@ const techStack = [
|
|||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<Footer slot="footer" />
|
||||
|
@ -993,6 +997,32 @@ const techStack = [
|
|||
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 */
|
||||
@media (max-width: 1024px) {
|
||||
.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
|
||||
// A component that displays terminal-like interface with animated commands and outputs
|
||||
|
||||
interface Command {
|
||||
export interface Props {
|
||||
title?: string;
|
||||
height?: string;
|
||||
showTitleBar?: boolean;
|
||||
showPrompt?: boolean;
|
||||
commands?: {
|
||||
prompt: string;
|
||||
command: string;
|
||||
output?: string[];
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
commands: Command[];
|
||||
title?: string;
|
||||
theme?: 'dark' | 'light';
|
||||
interactive?: boolean;
|
||||
showTitleBar?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
const {
|
||||
commands,
|
||||
title = "argobox:~/homelab",
|
||||
theme = "dark",
|
||||
interactive = false,
|
||||
showTitleBar = true
|
||||
title = "terminal",
|
||||
height = "auto",
|
||||
showTitleBar = true,
|
||||
showPrompt = true,
|
||||
commands = []
|
||||
} = Astro.props;
|
||||
|
||||
// Make the last command have the typing effect
|
||||
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 && (
|
||||
<div class="terminal-header">
|
||||
<div class="terminal-dots">
|
||||
|
@ -59,7 +54,7 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
</div>
|
||||
)}
|
||||
|
||||
<div class="terminal-content">
|
||||
<div class="terminal-content" style={`height: ${height};`}>
|
||||
{commands.map((cmd, index) => (
|
||||
<div class="terminal-block">
|
||||
<div class="terminal-line">
|
||||
|
@ -78,15 +73,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
</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>
|
||||
</div>
|
||||
|
@ -107,34 +93,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
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 */
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
|
@ -261,22 +219,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
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 */
|
||||
.terminal-cursor {
|
||||
position: absolute;
|
||||
|
@ -290,10 +232,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
.terminal-interactive:has(.terminal-input:focus) ~ .terminal-cursor {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Typing effect */
|
||||
.terminal-typing {
|
||||
position: relative;
|
||||
|
@ -335,13 +273,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
.terminal-box:hover .terminal-dot-green {
|
||||
background: #34d399;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.terminal-box {
|
||||
height: 300px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
@ -383,7 +314,8 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
const cursor = typingElement.closest('.terminal-box').querySelector('.terminal-cursor');
|
||||
if (cursor) {
|
||||
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';
|
||||
|
@ -394,215 +326,12 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typeWriter();
|
||||
}, 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
|
||||
const minButtons = document.querySelectorAll('.terminal-button-minimize');
|
||||
const maxButtons = document.querySelectorAll('.terminal-button-maximize');
|
||||
|
@ -610,24 +339,31 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
minButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const terminalBox = button.closest('.terminal-box');
|
||||
if (terminalBox) {
|
||||
terminalBox.classList.toggle('minimized');
|
||||
|
||||
if (terminalBox.classList.contains('minimized')) {
|
||||
const content = terminalBox.querySelector('.terminal-content');
|
||||
if (content) {
|
||||
terminalBox.dataset.prevHeight = terminalBox.style.height;
|
||||
terminalBox.style.height = '40px';
|
||||
content.style.display = 'none';
|
||||
}
|
||||
} else {
|
||||
const content = terminalBox.querySelector('.terminal-content');
|
||||
if (content) {
|
||||
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
|
||||
content.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
maxButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const terminalBox = button.closest('.terminal-box');
|
||||
if (terminalBox) {
|
||||
terminalBox.classList.toggle('maximized');
|
||||
|
||||
if (terminalBox.classList.contains('maximized')) {
|
||||
|
@ -644,7 +380,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
terminalBox.style.zIndex = '9999';
|
||||
terminalBox.style.borderRadius = '0';
|
||||
} else {
|
||||
const content = terminalBox.querySelector('.terminal-content');
|
||||
terminalBox.style.position = terminalBox.dataset.prevPosition || 'relative';
|
||||
terminalBox.style.width = terminalBox.dataset.prevWidth || '100%';
|
||||
terminalBox.style.height = terminalBox.dataset.prevHeight || '340px';
|
||||
|
@ -653,6 +388,7 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
|
|||
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" />
|
||||
|
||||
<!-- Theme CSS -->
|
||||
<link rel="stylesheet" href="/styles/theme.css" />
|
||||
<link rel="stylesheet" href="/src/styles/theme.css" />
|
||||
|
||||
<!-- Cytoscape Library for Knowledge Graph -->
|
||||
<script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>
|
||||
|
@ -360,5 +360,90 @@ const {
|
|||
}
|
||||
});
|
||||
</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>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,8 @@
|
|||
import BaseLayout from './BaseLayout.astro';
|
||||
import Header from '../components/Header.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 {
|
||||
frontmatter: {
|
||||
|
@ -11,20 +12,73 @@ interface Props {
|
|||
pubDate: Date;
|
||||
updatedDate?: Date;
|
||||
heroImage?: string;
|
||||
category?: string; // Keep category for potential filtering, but don't display in header
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
readTime?: string;
|
||||
draft?: boolean;
|
||||
author?: string; // Keep author field if needed elsewhere
|
||||
// Add other potential frontmatter fields as optional
|
||||
github?: string;
|
||||
live?: string;
|
||||
technologies?: string[];
|
||||
}
|
||||
author?: string;
|
||||
// Field for explicitly related posts
|
||||
related_posts?: 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', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
|
@ -47,10 +101,10 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
<div class="blog-post-container">
|
||||
<article class="blog-post">
|
||||
<header class="blog-post-header">
|
||||
{/* Display Draft Badge First */}
|
||||
{/* Display Draft Badge if needed */}
|
||||
{frontmatter.draft && <span class="draft-badge mb-4">DRAFT</span>}
|
||||
|
||||
{/* Title (Smaller) */}
|
||||
{/* Title */}
|
||||
<h1 class="blog-post-title mb-2">{frontmatter.title}</h1>
|
||||
|
||||
{/* Description */}
|
||||
|
@ -63,7 +117,7 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
<span class="blog-post-updated">(Updated {formattedUpdatedDate})</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>
|
||||
|
||||
{/* Tags */}
|
||||
|
@ -83,93 +137,199 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
</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 */}
|
||||
<div class="blog-post-content prose prose-invert max-w-none">
|
||||
<slot /> {/* Renders the actual markdown content */}
|
||||
</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>
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside class="blog-post-sidebar">
|
||||
{/* Author Card Updated */}
|
||||
{/* Author Card */}
|
||||
<div class="sidebar-card author-card">
|
||||
<div class="author-avatar">
|
||||
<img src="/images/avatar.jpg" alt="LaForceIT Tech Blogs" />
|
||||
<div class="avatar-placeholder">DL</div>
|
||||
</div>
|
||||
<div class="author-info">
|
||||
<h3>LaForceIT.com Tech Blogs</h3>
|
||||
<p>For Home Labbers, Technologists & Engineers</p>
|
||||
<h3>Daniel LaForce</h3>
|
||||
<p>Infrastructure & DevOps Engineer</p>
|
||||
</div>
|
||||
<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>
|
||||
{/* 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>
|
||||
|
||||
{/* Table of Contents Card */}
|
||||
<div class="sidebar-card toc-card">
|
||||
<h3>Table of Contents</h3>
|
||||
<nav class="toc-container" id="toc">
|
||||
<p class="text-sm text-gray-400">Loading TOC...</p>
|
||||
<p class="text-sm text-gray-400">Loading Table of Contents...</p>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Future Feature Placeholders remain commented out */}
|
||||
{/* ... */}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<Newsletter />
|
||||
<Footer slot="footer" />
|
||||
</BaseLayout>
|
||||
|
||||
{/* Script for Table of Contents Generation (Unchanged) */}
|
||||
<script>
|
||||
function generateToc() {
|
||||
// Table of Contents Generator
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const tocContainer = document.getElementById('toc');
|
||||
const contentArea = document.querySelector('.blog-post-content');
|
||||
|
||||
if (!tocContainer || !contentArea) return;
|
||||
|
||||
// Get all headings (h2, h3) from the content
|
||||
const headings = contentArea.querySelectorAll('h2, h3');
|
||||
if (headings.length > 0) {
|
||||
|
||||
if (headings.length === 0) {
|
||||
tocContainer.innerHTML = '<p class="toc-empty">No sections found in this article.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the TOC list
|
||||
const tocList = document.createElement('ul');
|
||||
tocList.className = 'toc-list';
|
||||
headings.forEach((heading) => {
|
||||
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;
|
||||
|
||||
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 = `#${id}`;
|
||||
link.href = `#${heading.id}`;
|
||||
link.textContent = heading.textContent;
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
|
||||
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);
|
||||
} else {
|
||||
tocContainer.innerHTML = '<p class="text-sm text-gray-400">No sections found.</p>';
|
||||
}
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', generateToc);
|
||||
} else {
|
||||
generateToc();
|
||||
}
|
||||
|
||||
// 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>
|
||||
|
||||
{/* 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>
|
||||
.blog-post-container {
|
||||
display: grid;
|
||||
grid-template-columns: 7fr 3fr;
|
||||
gap: 2rem;
|
||||
max-width: 1200px;
|
||||
margin: 2rem auto;
|
||||
padding: 0 1.5rem;
|
||||
}
|
||||
|
||||
.blog-post {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--card-border);
|
||||
overflow: hidden;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.blog-post-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.draft-badge {
|
||||
display: inline-block;
|
||||
margin-bottom: 1rem;
|
||||
|
@ -179,98 +339,162 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
font-size: 0.8rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 600;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.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;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.blog-post-title {
|
||||
/* Made title slightly smaller */
|
||||
font-size: clamp(1.8rem, 4vw, 2.5rem);
|
||||
line-height: 1.25; /* Adjusted line height */
|
||||
margin-bottom: 0.75rem; /* Adjusted margin */
|
||||
line-height: 1.2;
|
||||
margin-bottom: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.blog-post-description {
|
||||
font-size: 1.1rem;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 1.5rem; /* Increased margin */
|
||||
max-width: 75ch; /* Adjusted width */
|
||||
margin-bottom: 1.5rem;
|
||||
max-width: 75ch;
|
||||
}
|
||||
|
||||
.blog-post-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem 1.5rem;
|
||||
margin-bottom: 1.5rem; /* Increased margin */
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0.85rem;
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
margin-top: 0rem; /* Removed top margin */
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.blog-post-tag {
|
||||
color: var(--accent-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
transition: color 0.3s ease;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-family: var(--font-mono);
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.blog-post-tag:hover {
|
||||
color: var(--accent-primary);
|
||||
background-color: rgba(6, 182, 212, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.blog-post-hero {
|
||||
width: 100%;
|
||||
margin-bottom: 2.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--card-border);
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.blog-post-hero img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
.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 {
|
||||
position: sticky;
|
||||
top: 2rem;
|
||||
align-self: start;
|
||||
height: calc(100vh - 4rem);
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.sidebar-card {
|
||||
background: var(--card-bg);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Author Card */
|
||||
.author-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.author-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
@ -280,67 +504,92 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'
|
|||
border: 2px solid var(--accent-primary);
|
||||
background-color: var(--bg-secondary);
|
||||
}
|
||||
.author-avatar img {
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 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 {
|
||||
margin-bottom: 0.25rem;
|
||||
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);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.author-bio { /* Target the main bio */
|
||||
|
||||
.author-bio {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0; /* Remove bottom margin */
|
||||
margin-bottom: 1.5rem;
|
||||
color: var(--text-secondary);
|
||||
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 {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.toc-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: 60vh;
|
||||
|
||||
.toc-container {
|
||||
max-height: 500px;
|
||||
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) {
|
||||
.blog-post-container {
|
||||
grid-template-columns: 1fr; /* Stack on smaller screens */
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.blog-post-sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.blog-post-title {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.related-posts-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.blog-post {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -215,6 +215,9 @@ const commands = [
|
|||
});
|
||||
|
||||
// Function to create HTML for a single post card
|
||||
// Update the post card HTML creation function in the blog/index.astro file
|
||||
// Find the function that creates post cards (might be called createPostCardHTML)
|
||||
|
||||
function createPostCardHTML(post) {
|
||||
// Make sure tags is an array before stringifying
|
||||
const tagsString = JSON.stringify(post.tags || []);
|
||||
|
@ -949,4 +952,34 @@ const commands = [
|
|||
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>
|
|
@ -1,10 +1,11 @@
|
|||
---
|
||||
// src/pages/blog/index.astro - Blog page with enhanced knowledge graph and filtering
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'; // Corrected path
|
||||
import KnowledgeGraph from '../components/KnowledgeGraph.astro'; // Corrected path
|
||||
import Terminal from '../components/Terminal.astro'; // Corrected path
|
||||
import Header from '../components/Header.astro'; // Import Header
|
||||
import Footer from '../components/Footer.astro'; // Import Footer
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import KnowledgeGraph from '../components/KnowledgeGraph.astro';
|
||||
import Terminal from '../components/Terminal.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
|
||||
// Get all blog entries
|
||||
const allPosts = await getCollection('posts');
|
||||
|
@ -36,44 +37,64 @@ const postsData = sortedPosts.map(post => ({
|
|||
isDraft: post.data.draft || false
|
||||
}));
|
||||
|
||||
// Prepare graph data (Obsidian-style: Posts and Tags)
|
||||
const graphNodes = [];
|
||||
const graphEdges = [];
|
||||
const tagNodes = new Map(); // To avoid duplicate tag nodes
|
||||
|
||||
// Prepare enhanced graph data with both posts and tags
|
||||
const graphData = {
|
||||
nodes: [
|
||||
// Add post nodes
|
||||
sortedPosts.forEach(post => {
|
||||
if (!post.data.draft) { // Exclude drafts from graph
|
||||
graphNodes.push({
|
||||
...sortedPosts
|
||||
.filter(post => !post.data.draft)
|
||||
.map(post => ({
|
||||
id: post.slug,
|
||||
label: post.data.title,
|
||||
type: 'post', // Add type for styling/interaction
|
||||
url: `/posts/${post.slug}/` // Add URL for linking
|
||||
});
|
||||
type: 'post',
|
||||
category: post.data.category || 'Uncategorized',
|
||||
tags: post.data.tags || [],
|
||||
url: `/posts/${post.slug}/`
|
||||
})),
|
||||
|
||||
// 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({
|
||||
// Add tag nodes
|
||||
...allTags.map(tag => ({
|
||||
id: `tag-${tag}`,
|
||||
label: tag,
|
||||
type: 'tag',
|
||||
url: `/tag/${tag}/`
|
||||
}))
|
||||
],
|
||||
edges: []
|
||||
};
|
||||
|
||||
// Create edges between posts and their tags
|
||||
sortedPosts
|
||||
.filter(post => !post.data.draft)
|
||||
.forEach(post => {
|
||||
const postTags = post.data.tags || [];
|
||||
|
||||
// Add edges from post to tags
|
||||
postTags.forEach(tag => {
|
||||
graphData.edges.push({
|
||||
source: post.slug,
|
||||
target: tagId,
|
||||
type: 'tag-connection' // Add type
|
||||
target: `tag-${tag}`,
|
||||
type: 'post-tag',
|
||||
strength: 1
|
||||
});
|
||||
});
|
||||
|
||||
// Check if post references other posts (optional)
|
||||
// This requires a related_posts field in frontmatter
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const graphData = { nodes: graphNodes, edges: graphEdges };
|
||||
|
||||
// Terminal commands for tech effect
|
||||
const commands = [
|
||||
|
@ -84,8 +105,8 @@ const commands = [
|
|||
},
|
||||
{
|
||||
prompt: "[laforceit@argobox]$ ",
|
||||
command: "ls -la ./categories",
|
||||
output: allCategories.map(cat => `${cat}`)
|
||||
command: "ls -la ./tags",
|
||||
output: allTags.map(tag => `${tag}`)
|
||||
},
|
||||
{
|
||||
prompt: "[laforceit@argobox]$ ",
|
||||
|
@ -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">
|
||||
<Header slot="header" /> {/* Pass Header to slot */}
|
||||
<Header slot="header" />
|
||||
<main>
|
||||
{/* Hero Section with Terminal */}
|
||||
<!-- Hero Section with Terminal -->
|
||||
<section class="hero-section">
|
||||
<div class="hero-bg"></div>
|
||||
<div class="container">
|
||||
<div class="hero-content">
|
||||
<div class="hero-text">
|
||||
|
@ -121,36 +143,47 @@ const commands = [
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* Blog Posts Section */}
|
||||
<section class="blog-posts-section">
|
||||
<!-- Blog Content Section -->
|
||||
<section class="blog-content-section">
|
||||
<div class="container">
|
||||
<!-- Search and Filter Section with integrated Knowledge Graph -->
|
||||
<div class="search-filter-container">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">Latest Articles</h2>
|
||||
<h2 class="section-title">Knowledge Graph & Content Explorer</h2>
|
||||
<p class="section-description">
|
||||
Technical insights, infrastructure guides, and DevOps best practices
|
||||
Explore connections between articles and topics, or search by keyword
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search and Filter Section */}
|
||||
<!-- Knowledge Graph Visualization -->
|
||||
<div class="knowledge-graph-wrapper">
|
||||
<KnowledgeGraph graphData={graphData} height="500px" />
|
||||
</div>
|
||||
|
||||
<div class="search-filter-section">
|
||||
<div class="search-bar">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{/* Integrated Knowledge Graph */}
|
||||
<div class="integrated-graph-container">
|
||||
<KnowledgeGraph graphData={graphData} />
|
||||
{/* We will update graphData generation later */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Blog Grid (will be populated by JS) */}
|
||||
<!-- Blog Grid (populated by JS) -->
|
||||
<div class="blog-section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">All Articles</h2>
|
||||
<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>
|
||||
|
@ -158,9 +191,10 @@ const commands = [
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<Footer slot="footer" /> {/* Pass Footer to slot */}
|
||||
<Footer slot="footer" />
|
||||
|
||||
<!-- Client-side script for filtering and graph interactions -->
|
||||
<script define:vars={{ postsData, graphData }}>
|
||||
|
@ -168,101 +202,35 @@ const commands = [
|
|||
const searchInput = document.getElementById('search-input');
|
||||
const tagButtons = document.querySelectorAll('.tag-filter-btn');
|
||||
const blogGrid = document.getElementById('blog-grid');
|
||||
// Removed graphFilters as category filtering is removed from graph
|
||||
|
||||
// State variables
|
||||
let currentFilterTag = 'all';
|
||||
let currentSearchTerm = '';
|
||||
// Removed currentGraphFilter
|
||||
let cy; // Cytoscape instance will be set by KnowledgeGraph component
|
||||
|
||||
// Wait for cytoscape instance to be available
|
||||
document.addEventListener('graphReady', (e) => {
|
||||
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
|
||||
// Update the post card HTML creation function in the blog/index.astro file
|
||||
// Find the function that creates post cards (might be called createPostCardHTML)
|
||||
|
||||
function createPostCardHTML(post) {
|
||||
// Make sure tags is an array before stringifying
|
||||
const tagsString = JSON.stringify(post.tags || []);
|
||||
|
||||
// Create tag pills HTML
|
||||
const tagPills = post.tags.map(tag =>
|
||||
`<span class="post-tag">${tag}</span>`
|
||||
`<span class="post-tag" data-tag="${tag}">${tag}</span>`
|
||||
).join('');
|
||||
|
||||
return `
|
||||
<article class="post-card" data-tags='${tagsString}' data-slug="${post.slug}">
|
||||
<div class="post-card-inner">
|
||||
<a href="/posts/${post.slug}/" class="post-image-link">
|
||||
<div class="post-image-container">
|
||||
<img
|
||||
width="720"
|
||||
|
@ -274,6 +242,7 @@ const commands = [
|
|||
/>
|
||||
<div class="post-category-badge">${post.category}</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="post-content">
|
||||
<div class="post-meta">
|
||||
<time datetime="${post.pubDateISO}">${post.pubDate}</time>
|
||||
|
@ -314,7 +283,7 @@ const commands = [
|
|||
post.title.toLowerCase().includes(searchTermLower) ||
|
||||
post.description.toLowerCase().includes(searchTermLower) ||
|
||||
postTags.some(tag => tag.toLowerCase().includes(searchTermLower));
|
||||
return matchesTag && matchesSearch && !post.isDraft; // Exclude drafts
|
||||
return matchesTag && matchesSearch;
|
||||
});
|
||||
|
||||
// Update the grid HTML
|
||||
|
@ -322,46 +291,73 @@ const commands = [
|
|||
if (filteredPosts.length > 0) {
|
||||
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) {
|
||||
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
|
||||
cy.nodes().removeClass('highlighted').removeClass('faded');
|
||||
if (currentFilterTag !== 'all') {
|
||||
// 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 => {
|
||||
if (matchingPostSlugs.includes(node.id())) {
|
||||
node.removeClass('faded').addClass('highlighted');
|
||||
} else {
|
||||
node.removeClass('highlighted').addClass('faded'); // Fade non-matching posts
|
||||
}
|
||||
});
|
||||
|
||||
// Highlight tag nodes connected to visible posts OR the currently selected tag
|
||||
cy.nodes('[type="tag"]').forEach(tagNode => {
|
||||
const tagName = tagNode.id().replace(/^tag-/, '');
|
||||
const isSelectedTag = tagName === currentFilterTag;
|
||||
const isConnectedToVisiblePost = tagNode.connectedEdges().sources().some(postNode => matchingPostSlugs.includes(postNode.id()));
|
||||
|
||||
if (isSelectedTag || (currentFilterTag === 'all' && isConnectedToVisiblePost)) {
|
||||
// Highlight the tag node
|
||||
const tagNode = cy.getElementById(`tag-${currentFilterTag}`);
|
||||
if (tagNode.length > 0) {
|
||||
tagNode.removeClass('faded').addClass('highlighted');
|
||||
} else {
|
||||
tagNode.removeClass('highlighted').addClass('faded');
|
||||
}
|
||||
});
|
||||
|
||||
// Adjust edge visibility based on connected highlighted nodes
|
||||
cy.edges().forEach(edge => {
|
||||
if (edge.source().hasClass('highlighted') && edge.target().hasClass('highlighted')) {
|
||||
edge.removeClass('faded').addClass('highlighted');
|
||||
} else {
|
||||
edge.removeClass('highlighted').addClass('faded');
|
||||
// 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) {
|
||||
// We're searching - highlight matching posts
|
||||
cy.elements().addClass('faded').removeClass('highlighted filtered');
|
||||
|
||||
// Find and highlight matching post nodes
|
||||
matchingSlugs.forEach(slug => {
|
||||
const node = cy.getElementById(slug);
|
||||
if (node.length > 0) {
|
||||
node.removeClass('faded').addClass('highlighted');
|
||||
|
||||
// Also show connected tags
|
||||
const connectedTags = node.neighborhood('node[type="tag"]');
|
||||
connectedTags.removeClass('faded').addClass('filtered');
|
||||
|
||||
// And highlight edges
|
||||
node.connectedEdges().removeClass('faded');
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Reset graph view
|
||||
cy.elements().removeClass('faded highlighted filtered');
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
console.error("Blog grid element not found!");
|
||||
|
@ -388,12 +384,53 @@ const commands = [
|
|||
// Update filter and grid
|
||||
currentFilterTag = button.dataset.tag;
|
||||
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
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
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>
|
||||
</BaseLayout>
|
||||
|
@ -407,6 +444,32 @@ const commands = [
|
|||
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 {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
|
@ -465,17 +528,15 @@ const commands = [
|
|||
max-width: 560px;
|
||||
}
|
||||
|
||||
/* Graph Section */
|
||||
.graph-section {
|
||||
padding: 5rem 0;
|
||||
position: relative;
|
||||
background: linear-gradient(0deg, var(--bg-primary), var(--bg-secondary), var(--bg-primary));
|
||||
/* Blog Content Section */
|
||||
.blog-content-section {
|
||||
padding: 2rem 0 5rem;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
text-align: center;
|
||||
max-width: 800px;
|
||||
margin: 0 auto 3rem;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
|
@ -503,75 +564,56 @@ const commands = [
|
|||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
position: relative;
|
||||
height: 60vh;
|
||||
min-height: 500px;
|
||||
max-height: 800px;
|
||||
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;
|
||||
/* Search Filter Container with Knowledge Graph */
|
||||
.search-filter-container {
|
||||
margin-bottom: 4rem;
|
||||
background: rgba(15, 23, 42, 0.3);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border-primary);
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--font-size-sm);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.graph-filter:hover {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 10px var(--glow-primary);
|
||||
}
|
||||
|
||||
.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;
|
||||
.knowledge-graph-wrapper {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin: 0 0 1.5rem;
|
||||
}
|
||||
|
||||
.search-filter-section {
|
||||
margin-bottom: 3rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(13, 21, 41, 0.5);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 1.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
padding: 0.75rem 1rem 0.75rem 2.5rem;
|
||||
background-color: var(--bg-primary);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 6px;
|
||||
border-radius: 8px;
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
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 {
|
||||
|
@ -608,6 +650,7 @@ const commands = [
|
|||
background-color: rgba(226, 232, 240, 0.1);
|
||||
color: var(--text-primary);
|
||||
border-color: rgba(56, 189, 248, 0.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.tag-filter-btn.active {
|
||||
|
@ -617,19 +660,36 @@ const commands = [
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Styles for the integrated graph container */
|
||||
.integrated-graph-container {
|
||||
margin-top: 2rem; /* Add space above the graph */
|
||||
height: 400px; /* Adjust height as needed */
|
||||
border: 1px solid var(--border-primary);
|
||||
.graph-toggle-btn {
|
||||
position: absolute;
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
|
||||
color: var(--bg-primary);
|
||||
border: none;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 8px;
|
||||
background: rgba(15, 23, 42, 0.3); /* Slightly different background */
|
||||
position: relative; /* Needed for Cytoscape */
|
||||
overflow: hidden; /* Hide scrollbars if graph overflows */
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
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 {
|
||||
margin: 2rem 0 4rem;
|
||||
margin: 2rem 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 2rem;
|
||||
|
@ -661,6 +721,11 @@ const commands = [
|
|||
border-color: rgba(56, 189, 248, 0.4);
|
||||
}
|
||||
|
||||
.post-image-link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.post-image-container {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -669,6 +734,11 @@ const commands = [
|
|||
width: 100%;
|
||||
height: 200px;
|
||||
object-fit: cover;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
.post-card:hover .post-image {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.post-category-badge {
|
||||
|
@ -717,6 +787,18 @@ const commands = [
|
|||
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 {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
|
@ -742,12 +824,19 @@ const commands = [
|
|||
}
|
||||
|
||||
.post-tag {
|
||||
background: rgba(226, 232, 240, 0.05);
|
||||
color: var(--text-secondary);
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
color: #10B981;
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.7rem;
|
||||
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 {
|
||||
|
@ -766,6 +855,11 @@ const commands = [
|
|||
|
||||
.read-more::after {
|
||||
content: '→';
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.post-card:hover .read-more::after {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
|
@ -815,8 +909,19 @@ const commands = [
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.graph-container {
|
||||
height: 50vh;
|
||||
.hero-text {
|
||||
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 {
|
||||
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>
|
|
@ -1,17 +1,21 @@
|
|||
// 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';
|
||||
|
||||
export async function get() {
|
||||
// Get all posts
|
||||
const allPosts = await getCollection('posts', ({ data }) => {
|
||||
// Get content from all collections
|
||||
const posts = await getCollection('posts', ({ data }) => {
|
||||
// Exclude draft posts in production
|
||||
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
|
||||
const searchablePosts = allPosts.map(post => ({
|
||||
const searchablePosts = posts.map(post => ({
|
||||
slug: post.slug,
|
||||
title: post.data.title,
|
||||
description: post.data.description || '',
|
||||
|
@ -19,14 +23,60 @@ export async function get() {
|
|||
category: post.data.category || 'Uncategorized',
|
||||
tags: post.data.tags || [],
|
||||
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 {
|
||||
body: JSON.stringify(searchablePosts),
|
||||
body: JSON.stringify(allSearchableContent),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'max-age=3600'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,265 +1,483 @@
|
|||
---
|
||||
// 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 Header from '../../components/Header.astro';
|
||||
import Footer from '../../components/Footer.astro';
|
||||
import KnowledgeGraph from '../../components/KnowledgeGraph.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const allPosts = await getCollection('blog');
|
||||
const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())];
|
||||
try {
|
||||
// Get all posts
|
||||
const allPosts = await getCollection('posts', ({ data }) => {
|
||||
// Exclude draft posts in production
|
||||
return import.meta.env.PROD ? !data.draft : true;
|
||||
});
|
||||
|
||||
// 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 uniqueTags.map((tag) => {
|
||||
const filteredPosts = allPosts.filter((post) => post.data.tags.includes(tag));
|
||||
return {
|
||||
params: { tag },
|
||||
props: { posts: filteredPosts },
|
||||
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 { posts } = Astro.props;
|
||||
const { posts, allPosts } = Astro.props;
|
||||
|
||||
// Format date
|
||||
// Format dates
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
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)
|
||||
const sortedPosts = posts.sort((a, b) => {
|
||||
const dateA = new Date(a.data.pubDate);
|
||||
const dateB = new Date(b.data.pubDate);
|
||||
const sortedPosts = [...posts].sort((a, b) => {
|
||||
const dateA = new Date(a.data.pubDate || 0);
|
||||
const dateB = new Date(b.data.pubDate || 0);
|
||||
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}`}>
|
||||
<div class="container tag-page">
|
||||
<header class="tag-hero">
|
||||
<h1>Posts tagged with <span class="tag-highlight">{tag}</span></h1>
|
||||
<p>Explore {sortedPosts.length} {sortedPosts.length === 1 ? 'article' : 'articles'} related to {tag}</p>
|
||||
</header>
|
||||
<Header slot="header" />
|
||||
|
||||
<main class="tag-page-container">
|
||||
<section class="tag-hero">
|
||||
<div class="container">
|
||||
<h1>Posts tagged with <span class="tag-highlight">{tag}</span></h1>
|
||||
<p class="tag-description">
|
||||
Explore {sortedPosts.length} {sortedPosts.length === 1 ? 'article' : 'articles'} related to {tag}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="tag-content container">
|
||||
<div class="knowledge-graph-section">
|
||||
<h2>Content Connections</h2>
|
||||
<p class="section-description">
|
||||
Explore how {tag} relates to other content and tags
|
||||
</p>
|
||||
|
||||
<div class="graph-wrapper">
|
||||
<KnowledgeGraph graphData={graphData} height="400px" initialFilter="all" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="posts-section">
|
||||
<h2>Articles</h2>
|
||||
<div class="posts-grid">
|
||||
{sortedPosts.map((post) => (
|
||||
{sortedPosts.length > 0 ? sortedPosts.map((post) => (
|
||||
<article class="post-card">
|
||||
<!-- Simplified image rendering that works reliably -->
|
||||
<a href={`/posts/${post.slug}/`} class="post-card-link">
|
||||
{post.data.heroImage && (
|
||||
<div class="post-image-wrapper">
|
||||
<img
|
||||
width={720}
|
||||
height={360}
|
||||
src={post.data.heroImage || "/images/placeholders/default.jpg"}
|
||||
src={post.data.heroImage}
|
||||
alt=""
|
||||
class="post-image"
|
||||
width="400"
|
||||
height="225"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="post-content">
|
||||
<time datetime={post.data.pubDate}>{formatDate(post.data.pubDate)}</time>
|
||||
<h2 class="post-title">
|
||||
<a href={`/posts/${post.slug}/`}>{post.data.title}</a>
|
||||
</h2>
|
||||
<p class="post-excerpt">{post.data.description}</p>
|
||||
<div class="post-meta">
|
||||
<span class="reading-time">{post.data.minutesRead || '5 min'} read</span>
|
||||
<ul class="post-tags">
|
||||
{post.data.tags.map((tagName) => (
|
||||
<li>
|
||||
<a href={`/tag/${tagName}`} class={tagName === tag ? 'current-tag' : ''}>
|
||||
{tagName}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
<time datetime={post.data.pubDate?.toISOString()}>
|
||||
{formatDate(post.data.pubDate)}
|
||||
</time>
|
||||
{post.data.readTime && (
|
||||
<span class="read-time">{post.data.readTime}</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<a href="/tags" class="all-tags-link">
|
||||
<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">
|
||||
<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>
|
||||
|
||||
<div class="tag-navigation">
|
||||
<a href="/blog" class="back-button">
|
||||
<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>
|
||||
<polyline points="12 19 5 12 12 5"></polyline>
|
||||
</svg>
|
||||
View all tags
|
||||
Back to All Posts
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer slot="footer" />
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.tag-page {
|
||||
padding-top: 2rem;
|
||||
.tag-page-container {
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.tag-hero {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
.container {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
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 {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.tag-hero h1 {
|
||||
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;
|
||||
.post-card-link {
|
||||
display: flex;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
.post-image-wrapper {
|
||||
height: 200px;
|
||||
overflow: hidden;
|
||||
border-bottom: 1px solid var(--card-border);
|
||||
}
|
||||
|
||||
.post-image {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
height: 100%;
|
||||
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 {
|
||||
padding: 1.5rem;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
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;
|
||||
}
|
||||
|
||||
.post-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-top: auto;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-tertiary);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.reading-time {
|
||||
color: var(--text-tertiary);
|
||||
font-size: var(--font-size-sm);
|
||||
.read-time {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.post-tags li a {
|
||||
display: block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
background: rgba(56, 189, 248, 0.1);
|
||||
border-radius: 20px;
|
||||
.post-tag {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
background: rgba(226, 232, 240, 0.05);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.post-tag.current-tag {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: var(--accent-primary);
|
||||
font-size: var(--font-size-xs);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.post-tags li a:hover {
|
||||
background: rgba(56, 189, 248, 0.2);
|
||||
transform: translateY(-2px);
|
||||
.no-posts {
|
||||
grid-column: 1 / -1;
|
||||
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 {
|
||||
background: var(--accent-primary);
|
||||
.back-to-blog {
|
||||
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);
|
||||
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;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin: 0 auto;
|
||||
padding: 0.75rem 1.5rem;
|
||||
padding: 0.8rem 1.5rem;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 30px;
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-md);
|
||||
text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
width: fit-content;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.all-tags-link:hover {
|
||||
.back-button:hover {
|
||||
background: var(--bg-tertiary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.tag-hero h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
.tag-hero {
|
||||
padding: 4rem 0 2rem;
|
||||
}
|
||||
|
||||
.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 */
|
||||
}
|
||||
|
||||
/* Light Mode Variables */
|
||||
/* Enhanced Light Mode Variables - More tech-focused, less plain white */
|
||||
:root.light-mode {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8fafc; /* Lighter secondary */
|
||||
--bg-tertiary: #f1f5f9; /* Even lighter tertiary */
|
||||
--bg-code: #f1f5f9;
|
||||
--bg-primary: #f0f4f8; /* Subtle blue-gray instead of white */
|
||||
--bg-secondary: #e5eaf2; /* Slightly darker secondary */
|
||||
--bg-tertiary: #dae2ef; /* Even more blue tint for tertiary */
|
||||
--bg-code: #e5edf7;
|
||||
--text-primary: #1e293b; /* Darker primary text */
|
||||
--text-secondary: #475569; /* Darker secondary text */
|
||||
--text-tertiary: #64748b; /* Darker tertiary text */
|
||||
|
@ -52,14 +52,14 @@
|
|||
--glow-primary: rgba(8, 145, 178, 0.15);
|
||||
--glow-secondary: rgba(37, 99, 235, 0.15);
|
||||
--glow-tertiary: rgba(124, 58, 237, 0.15);
|
||||
--border-primary: rgba(0, 0, 0, 0.1); /* Darker borders */
|
||||
--border-secondary: rgba(0, 0, 0, 0.05);
|
||||
--card-bg: rgba(255, 255, 255, 0.8); /* White card with opacity */
|
||||
--card-border: rgba(37, 99, 235, 0.3); /* Blue border */
|
||||
--ui-element: #e2e8f0; /* Lighter UI elements */
|
||||
--ui-element-hover: #cbd5e1;
|
||||
--bg-primary-rgb: 255, 255, 255; /* RGB for gradients */
|
||||
--bg-secondary-rgb: 248, 250, 252; /* RGB for gradients */
|
||||
--border-primary: rgba(37, 99, 235, 0.15); /* More visible blue-tinted borders */
|
||||
--border-secondary: rgba(8, 145, 178, 0.1);
|
||||
--card-bg: rgba(255, 255, 255, 0.6); /* More transparent card background */
|
||||
--card-border: rgba(37, 99, 235, 0.2); /* Subtle blue border */
|
||||
--ui-element: rgba(226, 232, 240, 0.7); /* More transparent UI elements */
|
||||
--ui-element-hover: rgba(203, 213, 225, 0.8);
|
||||
--bg-primary-rgb: 240, 244, 248; /* RGB for gradients */
|
||||
--bg-secondary-rgb: 229, 234, 242; /* RGB for gradients */
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* Knowledge Graph specific theme adjustments */
|
||||
/* Knowledge Graph specific theme adjustments - More transparent in light mode */
|
||||
:root.light-mode .graph-container {
|
||||
background: rgba(248, 250, 252, 0.3);
|
||||
border: 1px solid var(--card-border);
|
||||
background: rgba(248, 250, 252, 0.08); /* Much more transparent - lighter gray */
|
||||
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 {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
background: rgba(255, 255, 255, 0.8); /* More opaque for readability */
|
||||
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 {
|
||||
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 {
|
||||
color: var(--text-secondary);
|
||||
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 {
|
||||
|
@ -92,7 +108,12 @@
|
|||
}
|
||||
|
||||
: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 */
|
||||
|
@ -100,6 +121,39 @@
|
|||
:root.light-mode code {
|
||||
background-color: var(--bg-code);
|
||||
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 */
|
||||
|
@ -130,3 +184,43 @@ input, select, textarea {
|
|||
background-color: var(--card-bg);
|
||||
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