From 21505d84d829437048f60ec5b718cf640d9bf34e Mon Sep 17 00:00:00 2001 From: Daniel LaForce Date: Wed, 23 Apr 2025 19:40:50 -0600 Subject: [PATCH 1/4] feat: Implement enhanced Knowledge Graph with post/tag connections --- backup-20250422/BaseLayout.astro | 112 ---- backup-20250422/blog-index.astro | 108 ---- backup-20250422/global.css | 832 ---------------------------- backup-20250422/index.astro | 566 ------------------- src/components/Footer.astro | 27 +- src/components/KnowledgeGraph.astro | 59 +- src/pages/search-index.json.js | 64 ++- 7 files changed, 129 insertions(+), 1639 deletions(-) delete mode 100644 backup-20250422/BaseLayout.astro delete mode 100644 backup-20250422/blog-index.astro delete mode 100644 backup-20250422/global.css delete mode 100644 backup-20250422/index.astro diff --git a/backup-20250422/BaseLayout.astro b/backup-20250422/BaseLayout.astro deleted file mode 100644 index e310117..0000000 --- a/backup-20250422/BaseLayout.astro +++ /dev/null @@ -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; ---- - - - - - - - {title} - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
-
-
-
- -
- - - -
- - - - \ No newline at end of file diff --git a/backup-20250422/blog-index.astro b/backup-20250422/blog-index.astro deleted file mode 100644 index 78715c6..0000000 --- a/backup-20250422/blog-index.astro +++ /dev/null @@ -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(); -}); ---- - - -
-
-

Blog

-

- Technical insights, infrastructure guides, and DevOps best practices from my home lab to production environments. -

-
- -
- {sortedPosts.map((post) => ( -
- {post.data.heroImage ? ( - - ) : ( - - )} -
- -

- {post.data.title} - {post.data.draft && Draft} -

-

{post.data.description}

- -
-
- ))} -
-
-
- - \ No newline at end of file diff --git a/backup-20250422/global.css b/backup-20250422/global.css deleted file mode 100644 index 975ec92..0000000 --- a/backup-20250422/global.css +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/backup-20250422/index.astro b/backup-20250422/index.astro deleted file mode 100644 index 15e70fb..0000000 --- a/backup-20250422/index.astro +++ /dev/null @@ -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()); ---- - - - -
-
-
Home Lab & DevOps
-

Exploring advanced infrastructure and automation

-

- Join me on a journey through enterprise-grade home lab setups, Kubernetes deployments, and DevOps best practices for the modern tech enthusiast. -

- - - Explore Latest Posts - -
- -
-
-
-
-
-
-
-
argobox:~/homelab
-
-
-
- $ - kubectl get nodes -
-
- NAME STATUS ROLES AGE VERSION
- argobox Ready <none> 47d v1.28.3+k3s1
- argobox-lite Ready control-plane,master 47d v1.28.3+k3s1 -
-
- $ - helm list -A -
-
- NAME NAMESPACE REVISION STATUS CHART
- cloudnative-pg postgres 1 deployed cloudnative-pg-0.18.0
- prometheus monitoring 2 deployed kube-prometheus-stack-51.2.0 -
-
- $ - cloudflared tunnel status -
-
-
-
- - -
-

My Digital Garden

-

- 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. -

- -
- - -
-
-

Latest Posts

-
- {posts.map((post) => ( -
- {post.data.heroImage ? ( - - ) : ( - - )} -
- -

- {post.data.title} - {post.data.draft && Draft} -

-

{post.data.description}

- -
-
- ))} -
-
- -
-

Configurations

-
- {configurations.map((config) => ( -
- {config.data.heroImage ? ( - - ) : ( - - )} -
- -

- {config.data.title} - {config.data.draft && Draft} -

-

{config.data.description}

- -
-
- ))} -
-
- -
-

Projects

-
- {projects.map((project) => ( -
- {project.data.heroImage ? ( - - ) : ( - - )} -
- -

- {project.data.title} - {project.data.draft && Draft} -

- - {project.data.technologies && ( -
- {project.data.technologies.map((tech) => ( - - ))} -
- )} - -

{project.data.description}

- -
-
- ))} -
-
- - - - - -
-

About Me

-
-
-

- 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. -

-

- 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! -

- -
-
-
-
-
- - diff --git a/src/components/Footer.astro b/src/components/Footer.astro index a177a2c..7310eca 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -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: '' }, { @@ -130,7 +130,14 @@ const services = [ diff --git a/src/components/KnowledgeGraph.astro b/src/components/KnowledgeGraph.astro index 854e098..7a45567 100644 --- a/src/components/KnowledgeGraph.astro +++ b/src/components/KnowledgeGraph.astro @@ -309,7 +309,13 @@ const nodeTypeCounts = { { selector: '.filtered', style: { 'background-color': 'data(color)', 'border-color': '#FFFFFF', 'border-width': '2px', 'color': '#FFFFFF', 'text-background-opacity': 0.8, 'opacity': 0.8, 'z-index': 15 } }, { selector: '.faded', style: { 'opacity': 0.15, 'text-opacity': 0.3, 'background-opacity': 0.3, 'z-index': 1 } }, { selector: 'node:selected', style: { 'border-width': '4px', 'border-color': '#FFFFFF', 'border-opacity': 1, 'background-color': 'data(color)', 'text-opacity': 1, 'color': '#FFFFFF', 'z-index': 30 } }, - { selector: 'edge:selected', style: { 'width': 'mapData(weight, 1, 10, 2, 6)', 'line-color': '#FFFFFF', 'opacity': 1, 'z-index': 30 } } + { selector: 'edge:selected', style: { 'width': 'mapData(weight, 1, 10, 2, 6)', 'line-color': '#FFFFFF', 'opacity': 1, 'z-index': 30 } }, + // Styles for dragging effects + { selector: 'node.grabbed', style: { + 'border-width': '3px', + 'border-color': '#FFFFFF', + 'border-opacity': 1 + }} ], // Update layout for better visualization of post-tag connections layout: { @@ -537,11 +543,20 @@ const nodeTypeCounts = { // Zoom controls document.getElementById('zoom-in')?.addEventListener('click', () => cy.zoom(cy.zoom() * 1.2)); document.getElementById('zoom-out')?.addEventListener('click', () => cy.zoom(cy.zoom() / 1.2)); + + // Reset button functionality document.getElementById('reset-graph')?.addEventListener('click', () => { - cy.fit(null, 30); + // Reset zoom and position + cy.fit(null, 30); + // Reset all filters and highlighting cy.elements().removeClass('faded highlighted filtered'); + // Reset active filter buttons const allFilterButton = document.querySelector('.graph-filter[data-filter="all"]'); - if (allFilterButton) allFilterButton.click(); + if (allFilterButton) allFilterButton.click(); + // Close node details panel if open + if (nodeDetailsEl) nodeDetailsEl.classList.remove('active'); + // Reset any selected nodes + cy.$(':selected').unselect(); }); // Add mouse wheel zoom controls @@ -561,6 +576,41 @@ const nodeTypeCounts = { } } + // Add Obsidian-like dragging behavior + // When dragging a node, connected nodes follow with a damping effect + cy.on('drag', 'node', function(e) { + const node = e.target; + const neighbors = node.neighborhood('node'); + + // Add grabbed class for styling + node.addClass('grabbed'); + + if (neighbors.length > 0) { + neighbors.forEach(neighbor => { + // Don't move nodes that are being manually dragged by the user + if (!neighbor.grabbed()) { + // Calculate the position to move the neighbor node + // This creates a "pull" effect where neighbors follow but with resistance + // The 0.2 factor controls how much the neighbor follows (smaller = less movement) + const damping = 0.2; + const dx = node.position('x') - neighbor.position('x'); + const dy = node.position('y') - neighbor.position('y'); + + // Apply the position change with damping + neighbor.position({ + x: neighbor.position('x') + dx * damping, + y: neighbor.position('y') + dy * damping + }); + } + }); + } + }); + + cy.on('dragfree', 'node', function(e) { + // Remove grabbed class when drag ends + e.target.removeClass('grabbed'); + }); + // Connect search input if it exists const searchInput = document.getElementById('search-input'); if (searchInput) { @@ -1107,4 +1157,5 @@ const nodeTypeCounts = { padding: 0.3rem 0.6rem; font-size: 0.75rem; } - } \ No newline at end of file + } + \ No newline at end of file diff --git a/src/pages/search-index.json.js b/src/pages/search-index.json.js index 8a2ff28..1a367aa 100644 --- a/src/pages/search-index.json.js +++ b/src/pages/search-index.json.js @@ -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' } - } + }; } \ No newline at end of file -- 2.40.1 From 81008d7b039cc7f85c5b56af584f45478e75e713 Mon Sep 17 00:00:00 2001 From: Daniel LaForce Date: Wed, 23 Apr 2025 23:58:54 -0600 Subject: [PATCH 2/4] feat: Implement major UI/UX updates and component refactors --- src/components/Header.astro | 88 +- src/components/HomePage.astro | 94 +- src/components/KnowledgeGraph.astro | 891 +++++++++++- src/components/MiniKnowledgeGraph.astro | 1752 +++++++++++++++++++++++ src/components/Terminal.astro | 398 +---- src/config/terminal.js | 414 ++++++ src/layouts/BaseLayout.astro | 87 +- src/layouts/BlogPost.astro | 1082 +++++++------- src/layouts/BlogPostLayout.astro | 181 ++- src/pages/blog/index.astro | 123 +- src/pages/index.astro | 923 +++++------- src/pages/tag/[tag].astro | 558 +++++--- src/pages/test-graph.astro | 179 +++ src/styles/theme.css | 134 +- 14 files changed, 5106 insertions(+), 1798 deletions(-) create mode 100644 src/components/MiniKnowledgeGraph.astro create mode 100644 src/config/terminal.js create mode 100644 src/pages/test-graph.astro diff --git a/src/components/Header.astro b/src/components/Header.astro index 65080b0..44539f9 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -423,9 +423,9 @@ 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) => { if (!query || query.length < 2) { @@ -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 => ` -
-
${post.title}
-
${post.description || ''}
-
- `).join(''); + searchResults.innerHTML = results.map(item => { + // Create type badge + let typeBadge = ''; + switch(item.type) { + case 'post': + typeBadge = 'Blog'; + break; + case 'project': + typeBadge = 'Project'; + break; + case 'configuration': + typeBadge = 'Config'; + break; + case 'external': + typeBadge = 'External'; + break; + default: + typeBadge = 'Content'; + } + + return ` +
+
+
${item.title}
+ ${typeBadge} +
+
${item.description || ''}
+ ${item.tags && item.tags.length > 0 ? + `
+ ${item.tags.slice(0, 3).map(tag => `${tag}`).join('')} +
` : '' + } +
+ `; + }).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 = '
No matching posts found
'; + searchResults.innerHTML = '
No matching content found
'; } } } catch (error) { @@ -479,8 +508,8 @@ const currentPath = Astro.url.pathname; } } }; - - // Search input event handler + + // Search input event handler with debounce let searchTimeout; searchInput?.addEventListener('input', (e) => { clearTimeout(searchTimeout); @@ -488,18 +517,19 @@ const currentPath = Astro.url.pathname; performSearch(e.target.value); }, 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 \ No newline at end of file diff --git a/src/components/HomePage.astro b/src/components/HomePage.astro index 8d87d97..86d3141 100644 --- a/src/components/HomePage.astro +++ b/src/components/HomePage.astro @@ -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 kubernetes/", - "drwxr-xr-x 3 laforceit users 4096 Apr 20 17:22 docker/", - "drwxr-xr-x 2 laforceit users 4096 Apr 19 14:30 networking/", - "drwxr-xr-x 4 laforceit users 4096 Apr 22 21:10 monitoring/", - "drwxr-xr-x 3 laforceit users 4096 Apr 21 16:45 storage/", - ] - }, - { - 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 = [ + + +
+
+
+
+ +
+
+ `
${item.prompt}
$ ${item.command}\n${item.output.join('\n')}`).join('\n\n')} + /> +
+
+
+
@@ -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 { diff --git a/src/components/KnowledgeGraph.astro b/src/components/KnowledgeGraph.astro index 7a45567..c1cad62 100644 --- a/src/components/KnowledgeGraph.astro +++ b/src/components/KnowledgeGraph.astro @@ -77,6 +77,32 @@ const nodeTypeCounts = {
+ + + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + +
+
@@ -160,8 +186,34 @@ const nodeTypeCounts = { +
+ + +
+
+

Knowledge Graph Explorer

+
+ +
+
+
+
+
+
+ +

Select a post in the graph to view its content here

+
+
+
+
+
diff --git a/src/components/MiniKnowledgeGraph.astro b/src/components/MiniKnowledgeGraph.astro new file mode 100644 index 0000000..dd4bded --- /dev/null +++ b/src/components/MiniKnowledgeGraph.astro @@ -0,0 +1,1752 @@ +--- +// src/components/MiniKnowledgeGraph.astro +// Enhanced smaller version of the Knowledge Graph for blog posts with Obsidian-like physics + +export interface GraphNode { + id: string; + label: string; + type: 'post' | 'tag' | 'category' | 'current'; + category?: string; + tags?: string[]; + url?: string; +} + +export interface GraphEdge { + source: string; + target: string; + type: 'post-tag' | 'post-category' | 'post-post'; + strength?: number; +} + +export interface GraphData { + nodes: GraphNode[]; + edges: GraphEdge[]; +} + +interface Props { + currentPost: any; // Current post data + relatedPosts?: any[]; // Optional related posts array + height?: string; +} + +const { currentPost, relatedPosts = [], height = "250px" } = Astro.props; + +// Prepare tags as nodes +const tagNodes = (currentPost.data.tags || []).map(tag => ({ + id: `tag-${tag}`, + label: tag, + type: 'tag', + url: `/tag/${tag}/` +})); + +// Create edges from the current post to its tags +const currentPostTagEdges = (currentPost.data.tags || []).map(tag => ({ + source: currentPost.slug, + target: `tag-${tag}`, + type: 'post-tag', + strength: 2 // Stronger connection for current post +})); + +// Create the current post node with a special type +const currentPostNode = { + id: currentPost.slug, + label: currentPost.data.title, + type: 'current', // Special type for styling + category: currentPost.data.category || 'Uncategorized', + tags: currentPost.data.tags || [], + url: `/posts/${currentPost.slug}/` +}; + +// Process related posts +const relatedPostNodes = relatedPosts.map(post => ({ + id: post.slug, + label: post.data.title, + type: 'post', + category: post.data.category || 'Uncategorized', + tags: post.data.tags || [], + url: `/posts/${post.slug}/` +})); + +// Create edges from related posts to their tags that are also current post tags +// This ensures we only show connections relevant to the current post +const relatedPostTagEdges = []; +relatedPosts.forEach(post => { + // For each tag that is also in current post, create an edge + (post.data.tags || []).filter(tag => + (currentPost.data.tags || []).includes(tag) + ).forEach(tag => { + relatedPostTagEdges.push({ + source: post.slug, + target: `tag-${tag}`, + type: 'post-tag', + strength: 1 + }); + }); + + // Also create edges between related posts and current post + relatedPostTagEdges.push({ + source: currentPost.slug, + target: post.slug, + type: 'post-post', + strength: 1 + }); +}); + +// Combine all nodes and edges +const graphData = { + nodes: [currentPostNode, ...tagNodes, ...relatedPostNodes], + edges: [...currentPostTagEdges, ...relatedPostTagEdges] +}; + +// Define node colors, radii and edge colors +const nodeColors = { + currentArticle: "#FF5733", + relatedArticle: "#3366CC", + tag: "#33CC66", + category: "#9966CC" // Add category color +}; + +const nodeRadii = { + currentArticle: 20, + relatedArticle: 15, + tag: 12, + category: 18 // Add category size +}; + +// Calculate node sizes +const nodeSizes = {}; +// Current post should be largest +nodeSizes[currentPost.slug] = 25; +// Tag sizes are middle +tagNodes.forEach(node => { + nodeSizes[node.id] = 18; +}); +// Related posts are smaller +relatedPostNodes.forEach(node => { + nodeSizes[node.id] = 20; +}); + +// Default physics settings with Obsidian-like behavior +const defaultPhysics = { + nodeRepulsion: 7500, // Increased to prevent nodes from getting too close + edgeElasticity: 0.35, // More flexible edges like Obsidian + gravity: 0.4, // Light gravity to keep nodes centered + animate: true, + damping: 0.12, // Mimics Obsidian's smooth drag effect + pullStrength: 0.09 // Strength of pull effect when dragging nodes +}; +--- + + + + + +
+

{title || 'Article Connections'}

+ + +
+ +
+

How to use the Knowledge Graph

+
    +
  • Click on nodes to see connections and navigate
  • +
  • Drag nodes to rearrange the graph
  • +
  • Use the zoom controls to zoom in/out
  • +
  • Click the fullscreen button for a larger view
  • +
  • Adjust physics settings with the physics controls
  • +
+
+
+ + +
+ + +
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + + + +
+ + + + + +
+
+ + Current Article +
+
+ + Related Articles +
+
+ + Category +
+
+ + Tags +
+
+ + +
+
+
Loading graph...
+
+ + +
+
+

Knowledge Graph Explorer

+
+ +
+
+
+
+
+
+ +

Select a post in the graph to view its content

+
+
+
+
+
+
+ + + + + + \ No newline at end of file diff --git a/src/components/Terminal.astro b/src/components/Terminal.astro index a0b99d2..5980720 100644 --- a/src/components/Terminal.astro +++ b/src/components/Terminal.astro @@ -2,37 +2,32 @@ // Terminal.astro // A component that displays terminal-like interface with animated commands and outputs -interface Command { - prompt: string; - command: string; - output?: string[]; - delay?: number; -} - -interface Props { - commands: Command[]; +export interface Props { title?: string; - theme?: 'dark' | 'light'; - interactive?: boolean; + height?: string; showTitleBar?: boolean; + showPrompt?: boolean; + commands?: { + prompt: string; + command: string; + output?: string[]; + delay?: number; + }[]; } -const { - commands, - title = "argobox:~/homelab", - theme = "dark", - interactive = false, - showTitleBar = true +const { + 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' : ''}`; --- -
+
{showTitleBar && (
@@ -59,7 +54,7 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
)} -
+
{commands.map((cmd, index) => (
@@ -78,15 +73,6 @@ const terminalClasses = `terminal-box ${theme === 'light' ? 'terminal-light' : '
))} - {interactive && ( -
-
- guest@argobox:~$ - -
-
- )} -
@@ -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; - } - } @@ -360,5 +360,90 @@ const { } }); + + + \ No newline at end of file diff --git a/src/layouts/BlogPost.astro b/src/layouts/BlogPost.astro index 9b4e09b..fa49613 100644 --- a/src/layouts/BlogPost.astro +++ b/src/layouts/BlogPost.astro @@ -1,542 +1,520 @@ --- -import BaseLayout from './BaseLayout.astro'; -import type { CollectionEntry } from 'astro:content'; +// The key change is moving the MiniKnowledgeGraph component BEFORE the tags section +// and styling it properly with clear z-index values to ensure proper display -type Props = { - title: string; - description: string; - pubDate: Date; - updatedDate?: Date; - heroImage?: string; - category?: string; - tags?: string[]; - draft?: boolean; +import BaseLayout from '../layouts/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'; +import { getCollection } from 'astro:content'; + +interface Props { + frontmatter: { + title: string; + description?: string; + pubDate: Date; + updatedDate?: Date; + heroImage?: string; + category?: string; + tags?: string[]; + readTime?: string; + draft?: boolean; + author?: string; + github?: string; + live?: string; + technologies?: string[]; + related_posts?: string[]; // Explicit related posts by slug + } +} + +const { frontmatter } = Astro.props; + +// Format dates +const formattedPubDate = frontmatter.pubDate ? new Date(frontmatter.pubDate).toLocaleDateString('en-us', { + year: 'numeric', + month: 'long', + day: 'numeric', +}) : 'N/A'; + +const formattedUpdatedDate = frontmatter.updatedDate ? new Date(frontmatter.updatedDate).toLocaleDateString('en-us', { + year: 'numeric', + month: 'long', + day: 'numeric', +}) : null; + +// Default image if heroImage is missing +const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'; + +// Get related posts for MiniKnowledgeGraph +// First get all posts +const allPosts = await getCollection('posts').catch(error => { + console.error('Error fetching posts collection:', error); + return []; +}); + +// Try blog collection if posts collection doesn't exist +const blogPosts = allPosts.length === 0 ? await getCollection('blog').catch(() => []) : []; +const combinedPosts = [...allPosts, ...blogPosts]; + +// Find the current post in collection +const currentPost = combinedPosts.find(post => + post.data.title === frontmatter.title || + post.slug === frontmatter.title.toLowerCase().replace(/\s+/g, '-') +); + +// Get related posts - first from explicit frontmatter relation, then by tag similarity +let relatedPosts = []; + +// If related_posts is specified in frontmatter, use those first +if (frontmatter.related_posts && frontmatter.related_posts.length > 0) { + const explicitRelatedPosts = combinedPosts.filter(post => + frontmatter.related_posts.includes(post.slug) + ); + relatedPosts = [...explicitRelatedPosts]; +} + +// If we need more related posts, find them by tags +if (relatedPosts.length < 3 && frontmatter.tags && frontmatter.tags.length > 0) { + // Calculate tag similarity score for each post + const tagSimilarityPosts = combinedPosts + .filter(post => + // Filter out current post and already included related posts + post.data.title !== frontmatter.title && + !relatedPosts.some(rp => rp.slug === post.slug) + ) + .map(post => { + // Count matching tags + const postTags = post.data.tags || []; + const matchingTags = postTags.filter(tag => + frontmatter.tags.includes(tag) + ); + return { + post, + score: matchingTags.length + }; + }) + .filter(item => item.score > 0) // Only consider posts with at least one matching tag + .sort((a, b) => b.score - a.score) // Sort by score descending + .map(item => item.post); // Extract just the post + + // Add tag-related posts to fill up to 3 related posts + relatedPosts = [...relatedPosts, ...tagSimilarityPosts.slice(0, 3 - relatedPosts.length)]; +} + +// Limit to 3 related posts +relatedPosts = relatedPosts.slice(0, 3); + +// Check if we can show the Knowledge Graph +const showKnowledgeGraph = currentPost || (frontmatter.tags?.length > 0 || relatedPosts.length > 0); + +// Create fallback data if current post is missing +const fallbackCurrentPost = currentPost || { + slug: frontmatter.title.toLowerCase().replace(/\s+/g, '-'), + data: { + title: frontmatter.title, + tags: frontmatter.tags || [], + category: frontmatter.category || 'Uncategorized' + } }; - -const { title, description, pubDate, updatedDate, heroImage, category, tags, draft } = Astro.props; - -// Format date with time zone -const formattedDate = pubDate ? new Date(pubDate).toLocaleDateString('en-us', { - year: 'numeric', - month: 'short', - day: 'numeric', -}) : ''; - -const formattedUpdatedDate = updatedDate ? new Date(updatedDate).toLocaleDateString('en-us', { - year: 'numeric', - month: 'short', - day: 'numeric', -}) : ''; - -// Social share URLs -const pageUrl = Astro.url.href; -const encodedUrl = encodeURIComponent(pageUrl); -const encodedTitle = encodeURIComponent(title); -const twitterShareUrl = `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`; -const linkedinShareUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`; -const facebookShareUrl = `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`; --- - -
-
- {draft && ( -
- - - - - Draft Post - Content may change -
- )} - - {heroImage && } - -
-

{title}

- - - - {tags && tags.length > 0 && ( - - )} -
+ +
-
- -
- - -
- - -
+
+
+
+ {/* Display Draft Badge First */} + {frontmatter.draft && DRAFT} + + {/* Title */} +

{frontmatter.title}

+ + {/* Description */} + {frontmatter.description &&

{frontmatter.description}

} + + {/* Metadata (Date, Read Time) */} + + + {/* Debug information */} +
+

Debug Info:

+

ShowKnowledgeGraph: {showKnowledgeGraph ? 'true' : 'false'}

+

CurrentPost exists: {currentPost ? 'yes' : 'no'}

+

Has Tags: {frontmatter.tags?.length > 0 ? 'yes' : 'no'}

+

Has Related Posts: {relatedPosts.length > 0 ? 'yes' : 'no'}

+

Current Post Tags: {JSON.stringify(frontmatter.tags || [])}

+
+ + {/* IMPORTANT CHANGE: Knowledge Graph - Display BEFORE tags */} + {showKnowledgeGraph && ( +
+ +
+ )} + + {/* Tags - Now placed AFTER the knowledge graph */} + {frontmatter.tags && frontmatter.tags.length > 0 && ( + + )} +
+ + {/* Display Hero Image */} + {displayImage && ( +
+ {frontmatter.title} +
+ )} + + {/* Main Content Area */} +
+ {/* Renders the actual markdown content */} +
+ +
+ + {/* Sidebar */} + +
+ + +
+{/* Script for Table of Contents Generation */} + + @@ -573,3 +551,53 @@ document.addEventListener('DOMContentLoaded', () => { tocContainer.appendChild(toc); }); + +{/* Script to ensure the knowledge graph initializes properly */} + diff --git a/src/layouts/BlogPostLayout.astro b/src/layouts/BlogPostLayout.astro index 62829d9..c3de0db 100644 --- a/src/layouts/BlogPostLayout.astro +++ b/src/layouts/BlogPostLayout.astro @@ -3,6 +3,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'; +import { getCollection } from 'astro:content'; interface Props { frontmatter: { @@ -11,20 +13,21 @@ 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 + author?: string; github?: string; live?: string; technologies?: string[]; + related_posts?: string[]; // Explicit related posts by slug } } const { frontmatter } = Astro.props; +// Format dates const formattedPubDate = frontmatter.pubDate ? new Date(frontmatter.pubDate).toLocaleDateString('en-us', { year: 'numeric', month: 'long', @@ -39,6 +42,58 @@ const formattedUpdatedDate = frontmatter.updatedDate ? new Date(frontmatter.upda // Default image if heroImage is missing const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'; + +// Get related posts for MiniKnowledgeGraph +// First get all posts +const allPosts = await getCollection('posts').catch(() => []); + +// Find the current post in collection +const currentPost = allPosts.find(post => + post.data.title === frontmatter.title || + post.slug === frontmatter.title.toLowerCase().replace(/\s+/g, '-') +); + +// Get related posts - first from explicit frontmatter relation, then by tag similarity +let relatedPosts = []; + +// If related_posts is specified in frontmatter, use those first +if (frontmatter.related_posts && frontmatter.related_posts.length > 0) { + const explicitRelatedPosts = allPosts.filter(post => + frontmatter.related_posts.includes(post.slug) + ); + relatedPosts = [...explicitRelatedPosts]; +} + +// If we need more related posts, find them by tags +if (relatedPosts.length < 3 && frontmatter.tags && frontmatter.tags.length > 0) { + // Calculate tag similarity score for each post + const tagSimilarityPosts = allPosts + .filter(post => + // Filter out current post and already included related posts + post.data.title !== frontmatter.title && + !relatedPosts.some(rp => rp.slug === post.slug) + ) + .map(post => { + // Count matching tags + const postTags = post.data.tags || []; + const matchingTags = postTags.filter(tag => + frontmatter.tags.includes(tag) + ); + return { + post, + score: matchingTags.length + }; + }) + .filter(item => item.score > 0) // Only consider posts with at least one matching tag + .sort((a, b) => b.score - a.score) // Sort by score descending + .map(item => item.post); // Extract just the post + + // Add tag-related posts to fill up to 3 related posts + relatedPosts = [...relatedPosts, ...tagSimilarityPosts.slice(0, 3 - relatedPosts.length)]; +} + +// Limit to 3 related posts +relatedPosts = relatedPosts.slice(0, 3); --- @@ -63,7 +118,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg' (Updated {formattedUpdatedDate}) )} {frontmatter.readTime && {frontmatter.readTime}} - {/* Category removed from display here */}
{/* Tags */} @@ -76,6 +130,18 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg' )}
+ {/* Content Connections Graph - only show if we have the current post and related content */} + {currentPost && (frontmatter.tags?.length > 0 || relatedPosts.length > 0) && ( +
+

Content Connections

+ +
+ )} + {/* Display Hero Image */} {displayImage && (
@@ -88,9 +154,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg' {/* Renders the actual markdown content */}
- {/* Future Feature Placeholders remain commented out */} - {/* ... */} - {/* Sidebar */} @@ -107,7 +170,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg'

Exploring enterprise-grade infrastructure, automation, Kubernetes, and zero-trust networking in the home lab and beyond.

- {/* Social links removed */} {/* Table of Contents Card */} @@ -118,8 +180,26 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg' - {/* Future Feature Placeholders remain commented out */} - {/* ... */} + {/* Related Posts */} + {relatedPosts.length > 0 && ( + + )} @@ -168,7 +248,6 @@ const displayImage = frontmatter.heroImage || '/images/placeholders/default.jpg' } -{/* Styles Updated */} \ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index fb4d564..cb55c68 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -5,6 +5,7 @@ import KnowledgeGraph from '../components/KnowledgeGraph.astro'; // Corrected pa 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 Newsletter from '../components/Newsletter.astro'; // Get all blog entries const allPosts = await getCollection('posts'); @@ -100,478 +101,158 @@ const commands = [ ]; --- - -
{/* Pass Header to slot */} -
- {/* Hero Section with Terminal */} + +
+ +
-
-
-
Technical Articles & Guides
-

Exploring advanced infrastructure and automation

-

- Dive into enterprise-grade home lab setups, Kubernetes deployments, and DevOps best practices for the modern tech enthusiast. -

-
-
- -
-
+

Knowledge Graph Explorer

+

+ Explore connections between articles and topics, or search by keyword +

- {/* Blog Posts Section */} -
+
-
-

Latest Articles

-

- Technical insights, infrastructure guides, and DevOps best practices -

+ +
+
- {/* Search and Filter Section */}
+
Filter by Tag: {allTags.map(tag => ( ))}
- - {/* Integrated Knowledge Graph */} -
- - {/* We will update graphData generation later */} -
- {/* Blog Grid (will be populated by JS) */} -
-
-
- Loading articles... -
+
+ {sortedPosts.map(post => ( +
+
+ +
+
+
+
+ + {new Date(post.data.pubDate).toLocaleDateString('en-us', { + year: 'numeric', + month: 'short', + day: 'numeric', + })} + + {post.data.readTime && {post.data.readTime}} +
+

+ {post.data.title} +

+
+

{post.data.description}

+
+
+ {(post.data.tags || []).map(tag => ( + + ))} +
+
+
+
+ ))}
-
{/* Pass Footer to slot */} - - + +
\ No newline at end of file + + + \ No newline at end of file diff --git a/src/pages/tag/[tag].astro b/src/pages/tag/[tag].astro index de4892a..77264f2 100644 --- a/src/pages/tag/[tag].astro +++ b/src/pages/tag/[tag].astro @@ -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())]; - - return uniqueTags.map((tag) => { - const filteredPosts = allPosts.filter((post) => post.data.tags.includes(tag)); - return { - params: { tag }, - props: { posts: filteredPosts }, - }; - }); + 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 { + params: { tag }, + props: { + posts: filteredPosts, + tag, + allPosts // Pass all posts for knowledge graph + }, + }; + }); + } catch (error) { + console.error("Error in getStaticPaths:", error); + // Return empty array as fallback + return []; + } } const { tag } = Astro.params; -const { 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 + })) + ) + ] +}; --- -
-
-

Posts tagged with {tag}

-

Explore {sortedPosts.length} {sortedPosts.length === 1 ? 'article' : 'articles'} related to {tag}

-
- -
- {sortedPosts.map((post) => ( -
- - -
- -

- {post.data.title} -

-

{post.data.description}

-
- ))} -
- - - - - - - View all tags - -
+ )} + + + + + +
+ +