fresh-main #5

Merged
KeyArgo merged 6 commits from fresh-main into main 2025-04-23 21:26:22 +00:00
13 changed files with 1603 additions and 254 deletions

View File

@ -7,7 +7,7 @@ import tailwind from '@astrojs/tailwind';
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: 'https://laforceit.blog', site: 'https://laforceit-blog.pages.dev', // Your current Cloudflare site
output: 'static', output: 'static',
// adapter: cloudflare(), // Commented out for local development // adapter: cloudflare(), // Commented out for local development
integrations: [ integrations: [
@ -17,10 +17,14 @@ export default defineConfig({
], ],
markdown: { markdown: {
shikiConfig: { shikiConfig: {
theme: 'dracula', theme: 'one-dark-pro',
wrap: true wrap: true
}, },
remarkPlugins: [], remarkPlugins: [],
rehypePlugins: [] rehypePlugins: []
},
compressHTML: false, // Disable HTML compression to avoid parsing errors
build: {
format: 'file', // Use 'file' instead of 'directory' format
} }
}); });

View File

@ -379,12 +379,13 @@ const navItems = [
</style> </style>
<script> <script>
// Handle mobile menu toggle // Only define one DOMContentLoaded event handler
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const menuBtn = document.getElementById('mobile-menu-btn'); const menuBtn = document.getElementById('mobile-menu-btn');
const mainNav = document.querySelector('.main-nav'); const mainNav = document.querySelector('.main-nav');
const header = document.querySelector('.site-header'); const header = document.querySelector('.site-header');
// Mobile menu toggle
if (menuBtn && mainNav) { if (menuBtn && mainNav) {
menuBtn.addEventListener('click', () => { menuBtn.addEventListener('click', () => {
mainNav.classList.toggle('active'); mainNav.classList.toggle('active');
@ -395,13 +396,13 @@ const navItems = [
// Header scroll effect // Header scroll effect
window.addEventListener('scroll', () => { window.addEventListener('scroll', () => {
if (window.scrollY > 50) { if (window.scrollY > 50) {
header.classList.add('scrolled'); header?.classList.add('scrolled');
} else { } else {
header.classList.remove('scrolled'); header?.classList.remove('scrolled');
} }
}); });
// Theme toggle functionality // Theme toggle functionality - only define once
const themeToggle = document.getElementById('theme-toggle'); const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) { if (themeToggle) {

View File

@ -0,0 +1,93 @@
---
// ThemeToggler.astro
// A component to toggle between light and dark themes
---
<button id="theme-toggle" aria-label="Toggle dark mode" class="theme-toggle">
<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" class="sun-icon">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
<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" class="moon-icon">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
<style>
.theme-toggle {
background: none;
border: none;
padding: 0.25rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-secondary);
cursor: pointer;
transition: color 0.3s ease, background-color 0.3s ease;
position: relative;
width: 34px;
height: 34px;
}
.theme-toggle:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.1);
}
.sun-icon, .moon-icon {
position: absolute;
transition: transform 0.5s ease, opacity 0.5s ease;
}
html:not(.dark) .sun-icon {
opacity: 1;
transform: rotate(0);
}
html:not(.dark) .moon-icon {
opacity: 0;
transform: rotate(90deg);
}
html.dark .sun-icon {
opacity: 0;
transform: rotate(-90deg);
}
html.dark .moon-icon {
opacity: 1;
transform: rotate(0);
}
</style>
<script>
// Theme toggling logic
document.addEventListener('DOMContentLoaded', () => {
const themeToggle = document.getElementById('theme-toggle');
// Function to set theme
const setTheme = (isDark) => {
if (isDark) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
};
// Theme toggle click handler
themeToggle?.addEventListener('click', () => {
const isDark = document.documentElement.classList.contains('dark');
setTheme(!isDark);
});
});
</script>

View File

@ -0,0 +1,65 @@
---
title: 'Getting Started with Infrastructure as Code'
description: 'Learn the basics of Infrastructure as Code and how to start using it in your projects.'
pubDate: '2023-11-15'
heroImage: '/images/placeholders/infrastructure.jpg'
categories: ['Infrastructure', 'DevOps']
tags: ['terraform', 'infrastructure', 'cloud', 'automation']
minutesRead: '5 min'
---
# Getting Started with Infrastructure as Code
Infrastructure as Code (IaC) is a key DevOps practice that involves managing and provisioning infrastructure through code instead of manual processes. This approach brings the same rigor, transparency, and version control to infrastructure that developers have long applied to application code.
## Why Infrastructure as Code?
IaC offers numerous benefits for modern DevOps teams:
- **Consistency**: Infrastructure deployments become reproducible and standardized
- **Version Control**: Track changes to your infrastructure just like application code
- **Automation**: Reduce manual errors and increase deployment speed
- **Documentation**: Your code becomes self-documenting
- **Testing**: Infrastructure can be tested before deployment
## Popular IaC Tools
There are several powerful tools for implementing IaC:
1. **Terraform**: Cloud-agnostic, works with multiple providers
2. **AWS CloudFormation**: Specific to AWS infrastructure
3. **Azure Resource Manager**: Microsoft's native IaC solution
4. **Google Cloud Deployment Manager**: For Google Cloud resources
5. **Pulumi**: Uses general-purpose programming languages
## Basic Terraform Example
Here's a simple example of Terraform code that provisions an AWS EC2 instance:
```hcl
provider "aws" {
region = "us-west-2"
}
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "Web Server"
Environment = "Development"
}
}
```
## Getting Started
To begin your IaC journey:
1. Choose a tool that fits your infrastructure needs
2. Start small with a simple resource
3. Learn about state management
4. Implement CI/CD for your infrastructure code
5. Consider using modules for reusability
Infrastructure as Code transforms how teams provision and manage resources, enabling more reliable, consistent deployments while reducing overhead and errors.

View File

@ -39,6 +39,16 @@ const baseSchema = z.object({
pubDate: z.union([z.string(), z.date(), z.null()]).optional().default(() => new Date()).transform(customDateParser), pubDate: z.union([z.string(), z.date(), z.null()]).optional().default(() => new Date()).transform(customDateParser),
updatedDate: z.union([z.string(), z.date(), z.null()]).optional().transform(val => val ? customDateParser(val) : undefined), updatedDate: z.union([z.string(), z.date(), z.null()]).optional().transform(val => val ? customDateParser(val) : undefined),
heroImage: z.string().optional().nullable(), heroImage: z.string().optional().nullable(),
// Add categories array that falls back to the single category field
categories: z.union([
z.array(z.string()),
z.string().transform(val => [val]),
z.null()
]).optional().transform(val => {
if (val === null || val === undefined) return ['Uncategorized'];
return val;
}),
// Keep the original category field for backward compatibility
category: z.string().optional().default('Uncategorized'), category: z.string().optional().default('Uncategorized'),
tags: z.union([z.array(z.string()), z.null()]).optional().default([]), tags: z.union([z.array(z.string()), z.null()]).optional().default([]),
draft: z.boolean().optional().default(false), draft: z.boolean().optional().default(false),
@ -49,7 +59,14 @@ const baseSchema = z.object({
github: z.string().optional(), github: z.string().optional(),
live: z.string().optional(), live: z.string().optional(),
technologies: z.array(z.string()).optional(), technologies: z.array(z.string()).optional(),
}).passthrough(); // Allow any other frontmatter properties }).passthrough() // Allow any other frontmatter properties
.transform(data => {
// If categories isn't set but category is, use category value to populate categories
if ((!data.categories || data.categories.length === 0) && data.category) {
data.categories = [data.category];
}
return data;
});
// Define collections using the same base schema // Define collections using the same base schema
const postsCollection = defineCollection({ const postsCollection = defineCollection({

View File

@ -0,0 +1,65 @@
---
title: 'Getting Started with Infrastructure as Code'
description: 'Learn the basics of Infrastructure as Code and how to start using it in your projects.'
pubDate: '2023-11-15'
heroImage: '/images/placeholders/infrastructure.jpg'
categories: ['Infrastructure', 'DevOps']
tags: ['terraform', 'infrastructure', 'cloud', 'automation']
minutesRead: '5 min'
---
# Getting Started with Infrastructure as Code
Infrastructure as Code (IaC) is a key DevOps practice that involves managing and provisioning infrastructure through code instead of manual processes. This approach brings the same rigor, transparency, and version control to infrastructure that developers have long applied to application code.
## Why Infrastructure as Code?
IaC offers numerous benefits for modern DevOps teams:
- **Consistency**: Infrastructure deployments become reproducible and standardized
- **Version Control**: Track changes to your infrastructure just like application code
- **Automation**: Reduce manual errors and increase deployment speed
- **Documentation**: Your code becomes self-documenting
- **Testing**: Infrastructure can be tested before deployment
## Popular IaC Tools
There are several powerful tools for implementing IaC:
1. **Terraform**: Cloud-agnostic, works with multiple providers
2. **AWS CloudFormation**: Specific to AWS infrastructure
3. **Azure Resource Manager**: Microsoft's native IaC solution
4. **Google Cloud Deployment Manager**: For Google Cloud resources
5. **Pulumi**: Uses general-purpose programming languages
## Basic Terraform Example
Here's a simple example of Terraform code that provisions an AWS EC2 instance:
```hcl
provider "aws" {
region = "us-west-2"
}
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "Web Server"
Environment = "Development"
}
}
```
## Getting Started
To begin your IaC journey:
1. Choose a tool that fits your infrastructure needs
2. Start small with a simple resource
3. Learn about state management
4. Implement CI/CD for your infrastructure code
5. Consider using modules for reusability
Infrastructure as Code transforms how teams provision and manage resources, enabling more reliable, consistent deployments while reducing overhead and errors.

View File

@ -23,6 +23,18 @@ const {
<title>{title}</title> <title>{title}</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
<!-- Theme initialization - Must be inline -->
<script is:inline>
// Initialize theme before page loads to prevent flash
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme === 'light' || (!savedTheme && !prefersDark)) {
document.documentElement.classList.add('light-mode');
} else {
document.documentElement.classList.remove('light-mode');
}
</script>
<!-- OpenGraph/Social Media Meta Tags --> <!-- OpenGraph/Social Media Meta Tags -->
<meta property="og:title" content={title} /> <meta property="og:title" content={title} />
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
@ -44,6 +56,9 @@ const {
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<!-- Theme CSS -->
<link rel="stylesheet" href="/styles/theme.css" />
<!-- Cytoscape Library for Knowledge Graph --> <!-- Cytoscape Library for Knowledge Graph -->
<script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script> <script src="https://unpkg.com/cytoscape@3.25.0/dist/cytoscape.min.js" is:inline></script>

View File

@ -1,38 +1,264 @@
--- ---
import { getCollection, getEntryBySlug } from 'astro:content'; // src/pages/blog/[slug].astro
import BlogPost from '../../layouts/BlogPost.astro'; import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
// Required getStaticPaths function for dynamic routes
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await getCollection('blog'); try {
return posts.map(post => ({ // Try first from 'blog' collection (auto-generated)
let allPosts = [];
try {
allPosts = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? !data.draft : true;
});
} catch (e) {
console.log('Blog collection not found, trying posts');
}
// If that fails or is empty, try 'posts' collection
if (allPosts.length === 0) {
allPosts = await getCollection('posts', ({ data }) => {
return import.meta.env.PROD ? !data.draft : true;
});
}
return allPosts.map(post => ({
params: { slug: post.slug }, params: { slug: post.slug },
props: { post }, props: { post },
})); }));
} catch (error) {
console.error('Error fetching posts:', error);
// Return empty array if both collections don't exist or are empty
return [];
}
} }
// Get the post from props
const { post } = Astro.props; const { post } = Astro.props;
const { Content } = await post.render();
// Handle undefined or null values // Format date helper
const title = post.data.title || ''; const formatDate = (date) => {
const description = post.data.description || ''; if (!date) return '';
const pubDate = post.data.pubDate ? new Date(post.data.pubDate) : new Date(); const d = new Date(date);
const updatedDate = post.data.updatedDate ? new Date(post.data.updatedDate) : undefined; return d.toLocaleDateString('en-US', {
const heroImage = post.data.heroImage || undefined; year: 'numeric',
const category = post.data.category || undefined; month: 'long',
const tags = post.data.tags || []; day: 'numeric'
const draft = post.data.draft || false; });
};
// Generate datetime attribute safely
const getISODate = (date) => {
if (!date) return '';
// Handle various date formats
try {
// If already a Date object
if (date instanceof Date) {
return date.toISOString();
}
// If it's a string or number, convert to Date
return new Date(date).toISOString();
} catch (error) {
// Fallback if date is invalid
console.error('Invalid date format:', date);
return '';
}
};
// Get the Content component for rendering markdown
const { Content } = await post.render();
--- ---
<BlogPost <BaseLayout title={post.data.title} description={post.data.description || ''}>
title={title} <article class="container blog-post">
description={description} <header class="post-header">
pubDate={pubDate} <h1>{post.data.title}</h1>
updatedDate={updatedDate} <div class="post-meta">
heroImage={heroImage} {post.data.pubDate && <time datetime={getISODate(post.data.pubDate)}>{formatDate(post.data.pubDate)}</time>}
category={category} {post.data.updatedDate && <div class="updated-date">Updated: {formatDate(post.data.updatedDate)}</div>}
tags={tags} {post.data.readTime && <div class="read-time">{post.data.readTime} read</div>}
draft={draft} {post.data.author && <div class="author">By {post.data.author}</div>}
> </div>
</header>
{post.data.heroImage && (
<div class="hero-image">
<img src={post.data.heroImage} alt={post.data.title} />
</div>
)}
<div class="post-content">
<div class="post-body">
<Content /> <Content />
</BlogPost> </div>
<aside class="post-sidebar">
{post.data.tags && post.data.tags.length > 0 && (
<div class="tags-section">
<h3>Tags</h3>
<div class="tags">
{post.data.tags.map(tag => (
<a href={`/tag/${tag}`} class="tag">{tag}</a>
))}
</div>
</div>
)}
<div class="category-section">
<h3>Category</h3>
<a href={`/categories/${post.data.category || 'Uncategorized'}`} class="category">
{post.data.category || 'Uncategorized'}
</a>
</div>
{post.data.categories && post.data.categories.length > 0 && (
<div class="categories-section">
<h3>Categories</h3>
<div class="categories">
{post.data.categories.map(category => (
<a href={`/categories/${category}`} class="category">
{category}
</a>
))}
</div>
</div>
)}
</aside>
</div>
</article>
</BaseLayout>
<style>
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 var(--container-padding, 1.5rem);
}
.blog-post {
padding: 2rem 0;
}
.post-header {
margin-bottom: 2rem;
text-align: center;
}
.post-header h1 {
font-size: var(--font-size-4xl, 2.25rem);
margin-bottom: 1rem;
line-height: 1.2;
}
.post-meta {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 1rem;
color: var(--text-tertiary);
font-size: var(--font-size-sm, 0.875rem);
font-family: var(--font-mono);
}
.hero-image {
margin-bottom: 2rem;
border-radius: 12px;
overflow: hidden;
border: 1px solid var(--border-primary);
}
.hero-image img {
width: 100%;
height: auto;
display: block;
}
.post-content {
display: grid;
grid-template-columns: 3fr 1fr;
gap: 2rem;
}
.post-body {
background: var(--card-bg);
border-radius: 12px;
padding: 2rem;
border: 1px solid var(--border-primary);
}
.post-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.tags-section, .category-section, .categories-section {
background: var(--card-bg);
border-radius: 8px;
padding: 1.5rem;
border: 1px solid var(--border-primary);
}
.tags-section h3, .category-section h3, .categories-section h3 {
font-size: var(--font-size-lg, 1.125rem);
margin-bottom: 1rem;
color: var(--text-primary);
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
display: inline-block;
padding: 0.25rem 0.75rem;
background: rgba(56, 189, 248, 0.1);
border-radius: 20px;
color: var(--accent-primary);
font-size: var(--font-size-xs, 0.75rem);
text-decoration: none;
transition: all 0.2s ease;
}
.tag:hover {
background: rgba(56, 189, 248, 0.2);
transform: translateY(-2px);
}
.category, .categories {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.category {
display: inline-block;
padding: 0.5rem 1rem;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
border-radius: 20px;
color: var(--bg-primary);
font-size: var(--font-size-sm, 0.875rem);
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
text-align: center;
}
.category:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.post-content {
grid-template-columns: 1fr;
}
.post-header h1 {
font-size: var(--font-size-2xl, 1.5rem);
}
}
</style>

View File

@ -0,0 +1,294 @@
---
// src/pages/categories/[category].astro
import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() {
try {
// Get posts from the posts collection
const allPosts = await getCollection('posts', ({ data }) => {
// Exclude draft posts in production
return import.meta.env.PROD ? !data.draft : true;
});
// Extract all categories from posts
const allCategories = new Set<string>();
allPosts.forEach(post => {
// Handle both single category and categories array
if (post.data.categories && Array.isArray(post.data.categories)) {
post.data.categories.forEach(cat => allCategories.add(cat));
} else if (post.data.category) {
allCategories.add(post.data.category);
} else {
allCategories.add('Uncategorized');
}
});
// Convert to array and sort alphabetically
const uniqueCategories = Array.from(allCategories).sort();
// If there are no categories, provide a default path
if (uniqueCategories.length === 0) {
return [{
params: { category: 'general' },
props: { posts: [] }
}];
}
// Create a path for each category
return uniqueCategories.map(category => {
// Filter posts for this category
const filteredPosts = allPosts.filter(post => {
if (post.data.categories && Array.isArray(post.data.categories)) {
return post.data.categories.includes(category);
}
return post.data.category === category;
});
return {
params: { category },
props: { posts: filteredPosts }
};
});
} catch (error) {
console.error('Error generating category pages:', error);
// Fallback to ensure build doesn't fail
return [{
params: { category: 'general' },
props: { posts: [] }
}];
}
}
const { category } = Astro.params;
const { posts } = Astro.props;
// Format date function
const formatDate = (dateStr) => {
if (!dateStr) return '';
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
};
---
<BaseLayout title={`${category} | LaForce IT Blog`} description={`Articles in the ${category} category`}>
<div class="container">
<div class="category-header">
<h1>Posts in category: <span class="category-name">{category}</span></h1>
<p class="category-description">Browse {posts.length} {posts.length === 1 ? 'article' : 'articles'} in this category</p>
</div>
{posts.length > 0 ? (
<div class="posts-grid">
{posts.map((post) => (
<article class="post-card">
<img
width={720}
height={360}
src={post.data.heroImage || "/images/placeholders/default.jpg"}
alt=""
class="post-image"
/>
<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 || post.data.excerpt || ''}</p>
<div class="post-meta">
<span class="reading-time">{post.data.readTime || '5 min'} read</span>
{post.data.tags && post.data.tags.length > 0 && (
<div class="post-tags">
{post.data.tags.slice(0, 3).map((tag) => (
<a href={`/tag/${tag}`} class="tag-link">
{tag}
</a>
))}
</div>
)}
</div>
</div>
</article>
))}
</div>
) : (
<div class="empty-state">
<p>No posts in this category yet. Check back soon!</p>
<a href="/blog" class="back-to-blog">Browse all posts</a>
</div>
)}
</div>
</BaseLayout>
<style>
.container {
padding: 2rem 0;
max-width: 1280px;
margin: 0 auto;
padding: 0 var(--container-padding, 1.5rem);
}
.category-header {
text-align: center;
margin: 2rem 0 4rem;
}
.category-name {
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;
}
.category-description {
color: var(--text-secondary);
font-size: var(--font-size-lg, 1.125rem);
margin-top: 0.5rem;
}
.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;
display: flex;
flex-direction: column;
height: 100%;
}
.post-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
border-color: var(--accent-primary);
}
.post-image {
width: 100%;
height: 200px;
object-fit: cover;
border-bottom: 1px solid var(--border-primary);
}
.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, 0.875rem);
font-family: var(--font-mono);
}
.post-title {
font-size: var(--font-size-xl, 1.25rem);
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, 1rem);
margin-bottom: 1.5rem;
line-height: 1.6;
flex-grow: 1;
}
.post-meta {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: auto;
}
.reading-time {
color: var(--text-tertiary);
font-size: var(--font-size-sm, 0.875rem);
font-family: var(--font-mono);
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag-link {
display: inline-block;
padding: 0.25rem 0.75rem;
background: rgba(56, 189, 248, 0.1);
border-radius: 20px;
color: var(--accent-primary);
font-size: var(--font-size-xs, 0.75rem);
text-decoration: none;
transition: all 0.2s ease;
}
.tag-link:hover {
background: rgba(56, 189, 248, 0.2);
transform: translateY(-2px);
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: var(--card-bg);
border-radius: 12px;
border: 1px solid var(--border-primary);
}
.empty-state p {
color: var(--text-secondary);
margin-bottom: 1.5rem;
}
.back-to-blog {
display: inline-block;
padding: 0.75rem 1.5rem;
background: var(--accent-primary);
color: var(--bg-primary);
border-radius: 6px;
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
}
.back-to-blog:hover {
background: var(--accent-secondary);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.posts-grid {
grid-template-columns: 1fr;
}
.category-header {
margin: 1rem 0 2rem;
}
}
</style>

View File

@ -0,0 +1,225 @@
---
// src/pages/configurations/[slug].astro
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
// Required getStaticPaths function for dynamic routes
export async function getStaticPaths() {
try {
const configEntries = await getCollection('configurations', ({ data }) => {
// Filter out drafts in production
return import.meta.env.PROD ? !data.draft : true;
});
return configEntries.map(entry => ({
params: { slug: entry.slug },
props: { entry },
}));
} catch (error) {
console.error('Error fetching configurations:', error);
// Return empty array if collection doesn't exist or is empty
return [];
}
}
// Get the configuration from props
const { entry } = Astro.props;
// Format date helper
const formatDate = (date) => {
if (!date) return '';
const d = new Date(date);
return d.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
// Generate datetime attribute safely
const getISODate = (date) => {
if (!date) return '';
// Handle various date formats
try {
// If already a Date object
if (date instanceof Date) {
return date.toISOString();
}
// If it's a string or number, convert to Date
return new Date(date).toISOString();
} catch (error) {
// Fallback if date is invalid
console.error('Invalid date format:', date);
return '';
}
};
// Get the Content component for rendering markdown
const { Content } = await entry.render();
---
<BaseLayout title={entry.data.title} description={entry.data.description || ''}>
<article class="container configuration-detail">
<header class="configuration-header">
<h1>{entry.data.title}</h1>
{entry.data.pubDate && <time datetime={getISODate(entry.data.pubDate)}>{formatDate(entry.data.pubDate)}</time>}
{entry.data.updatedDate && <div class="updated-date">Updated: {formatDate(entry.data.updatedDate)}</div>}
</header>
<div class="configuration-content">
<div class="configuration-body">
<Content />
</div>
<aside class="configuration-sidebar">
{entry.data.heroImage && (
<div class="configuration-image">
<img src={entry.data.heroImage} alt={entry.data.title} />
</div>
)}
{entry.data.tags && entry.data.tags.length > 0 && (
<div class="tags-section">
<h3>Tags</h3>
<div class="tags">
{entry.data.tags.map(tag => (
<a href={`/tag/${tag}`} class="tag">{tag}</a>
))}
</div>
</div>
)}
<div class="category-section">
<h3>Category</h3>
<a href={`/categories/${entry.data.category || 'Uncategorized'}`} class="category">
{entry.data.category || 'Uncategorized'}
</a>
</div>
</aside>
</div>
</article>
</BaseLayout>
<style>
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 var(--container-padding, 1.5rem);
}
.configuration-detail {
padding: 2rem 0;
}
.configuration-header {
margin-bottom: 2rem;
text-align: center;
}
.configuration-header h1 {
font-size: var(--font-size-3xl, 1.875rem);
margin-bottom: 0.5rem;
line-height: 1.2;
}
.configuration-header time, .updated-date {
color: var(--text-tertiary);
font-size: var(--font-size-sm, 0.875rem);
font-family: var(--font-mono);
}
.updated-date {
margin-top: 0.25rem;
}
.configuration-content {
display: grid;
grid-template-columns: 3fr 1fr;
gap: 2rem;
}
.configuration-body {
background: var(--card-bg);
border-radius: 12px;
padding: 2rem;
border: 1px solid var(--border-primary);
}
.configuration-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.configuration-image {
margin-bottom: 1.5rem;
}
.configuration-image img {
width: 100%;
border-radius: 8px;
border: 1px solid var(--border-primary);
}
.tags-section, .category-section {
background: var(--card-bg);
border-radius: 8px;
padding: 1.5rem;
border: 1px solid var(--border-primary);
}
.tags-section h3, .category-section h3 {
font-size: var(--font-size-lg, 1.125rem);
margin-bottom: 1rem;
color: var(--text-primary);
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
display: inline-block;
padding: 0.25rem 0.75rem;
background: rgba(56, 189, 248, 0.1);
border-radius: 20px;
color: var(--accent-primary);
font-size: var(--font-size-xs, 0.75rem);
text-decoration: none;
transition: all 0.2s ease;
}
.tag:hover {
background: rgba(56, 189, 248, 0.2);
transform: translateY(-2px);
}
.category {
display: inline-block;
padding: 0.5rem 1rem;
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
border-radius: 20px;
color: var(--bg-primary);
font-size: var(--font-size-sm, 0.875rem);
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
}
.category:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.configuration-content {
grid-template-columns: 1fr;
}
.configuration-header h1 {
font-size: var(--font-size-2xl, 1.5rem);
}
}
</style>

View File

@ -0,0 +1,248 @@
---
// src/pages/projects/[slug].astro
import { getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
// Required getStaticPaths function for dynamic routes
export async function getStaticPaths() {
try {
const projectEntries = await getCollection('projects', ({ data }) => {
// Filter out drafts in production
return import.meta.env.PROD ? !data.draft : true;
});
return projectEntries.map(entry => ({
params: { slug: entry.slug },
props: { entry },
}));
} catch (error) {
console.error('Error fetching projects:', error);
// Return empty array if collection doesn't exist or is empty
return [];
}
}
// Get the project from props
const { entry } = Astro.props;
// Format date helper
const formatDate = (date) => {
if (!date) return '';
const d = new Date(date);
return d.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
// Generate datetime attribute safely
const getISODate = (date) => {
if (!date) return '';
// Handle various date formats
try {
// If already a Date object
if (date instanceof Date) {
return date.toISOString();
}
// If it's a string or number, convert to Date
return new Date(date).toISOString();
} catch (error) {
// Fallback if date is invalid
console.error('Invalid date format:', date);
return '';
}
};
// Get the Content component for rendering markdown
const { Content } = await entry.render();
---
<BaseLayout title={entry.data.title} description={entry.data.description || ''}>
<article class="container project-detail">
<header class="project-header">
<h1>{entry.data.title}</h1>
{entry.data.pubDate && <time datetime={getISODate(entry.data.pubDate)}>{formatDate(entry.data.pubDate)}</time>}
{entry.data.updatedDate && <div class="updated-date">Updated: {formatDate(entry.data.updatedDate)}</div>}
</header>
<div class="project-content">
<div class="project-body">
<Content />
</div>
<aside class="project-sidebar">
{entry.data.heroImage && (
<div class="project-image">
<img src={entry.data.heroImage} alt={entry.data.title} />
</div>
)}
{entry.data.technologies && entry.data.technologies.length > 0 && (
<div class="tech-section">
<h3>Technologies</h3>
<div class="technologies">
{entry.data.technologies.map(tech => (
<span class="technology">{tech}</span>
))}
</div>
</div>
)}
<div class="project-links">
{entry.data.github && (
<a href={entry.data.github} target="_blank" rel="noopener noreferrer" class="project-link github">
<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">
<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 Repository
</a>
)}
{entry.data.live && (
<a href={entry.data.live} target="_blank" rel="noopener noreferrer" class="project-link live">
<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">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
Live Demo
</a>
)}
</div>
</aside>
</div>
</article>
</BaseLayout>
<style>
.container {
max-width: 1280px;
margin: 0 auto;
padding: 0 var(--container-padding, 1.5rem);
}
.project-detail {
padding: 2rem 0;
}
.project-header {
margin-bottom: 2rem;
text-align: center;
}
.project-header h1 {
font-size: var(--font-size-3xl, 1.875rem);
margin-bottom: 0.5rem;
line-height: 1.2;
}
.project-header time, .updated-date {
color: var(--text-tertiary);
font-size: var(--font-size-sm, 0.875rem);
font-family: var(--font-mono);
}
.updated-date {
margin-top: 0.25rem;
}
.project-content {
display: grid;
grid-template-columns: 3fr 1fr;
gap: 2rem;
}
.project-body {
background: var(--card-bg);
border-radius: 12px;
padding: 2rem;
border: 1px solid var(--border-primary);
}
.project-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.project-image {
margin-bottom: 1.5rem;
}
.project-image img {
width: 100%;
border-radius: 8px;
border: 1px solid var(--border-primary);
}
.tech-section, .project-links {
background: var(--card-bg);
border-radius: 8px;
padding: 1.5rem;
border: 1px solid var(--border-primary);
}
.tech-section h3 {
font-size: var(--font-size-lg, 1.125rem);
margin-bottom: 1rem;
color: var(--text-primary);
}
.technologies {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.technology {
display: inline-block;
padding: 0.25rem 0.75rem;
background: rgba(56, 189, 248, 0.1);
border-radius: 20px;
color: var(--accent-primary);
font-size: var(--font-size-xs, 0.75rem);
}
.project-links {
display: flex;
flex-direction: column;
gap: 1rem;
}
.project-link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
border-radius: 8px;
text-decoration: none;
font-weight: 500;
transition: all 0.3s ease;
}
.github {
background: #24292e;
color: white;
}
.live {
background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary));
color: white;
}
.project-link:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
@media (max-width: 768px) {
.project-content {
grid-template-columns: 1fr;
}
.project-header h1 {
font-size: var(--font-size-2xl, 1.5rem);
}
}
</style>

View File

@ -1,216 +1,184 @@
import { getCollection } from 'astro:content'; ---
// src/pages/tag/[tag].astro
// Dynamic route for tag pages
import BaseLayout from '../../layouts/BaseLayout.astro'; import BaseLayout from '../../layouts/BaseLayout.astro';
import { getCollection } from 'astro:content';
export async function getStaticPaths() { export async function getStaticPaths() {
const allPosts = await getCollection('posts'); const allPosts = await getCollection('blog');
const uniqueTags = [...new Set(allPosts.map((post) => post.data.tags).flat())];
// Get all unique tags from all posts
const uniqueTags = [...new Set(allPosts.flatMap(post => post.data.tags || []))];
// Create a page for each tag
return uniqueTags.map(tag => {
// Filter posts that have this tag
const filteredPosts = allPosts.filter(post =>
post.data.tags && post.data.tags.includes(tag)
);
return uniqueTags.map((tag) => {
const filteredPosts = allPosts.filter((post) => post.data.tags.includes(tag));
return { return {
params: { tag }, params: { tag },
props: { posts: filteredPosts, tag }, props: { posts: filteredPosts },
}; };
}); });
} }
const { posts, tag } = Astro.props; const { tag } = Astro.params;
const { posts } = Astro.props;
// Sort posts by date // Format date
const formatDate = (dateStr) => {
const date = new Date(dateStr);
return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
};
// Sort posts by date (newest first)
const sortedPosts = posts.sort((a, b) => { const sortedPosts = posts.sort((a, b) => {
const dateA = a.data.pubDate ? new Date(a.data.pubDate) : new Date(0); const dateA = new Date(a.data.pubDate);
const dateB = b.data.pubDate ? new Date(b.data.pubDate) : new Date(0); const dateB = new Date(b.data.pubDate);
return dateB.getTime() - dateA.getTime(); return dateB.getTime() - dateA.getTime();
}); });
--- ---
<BaseLayout title={`Posts tagged with "${tag}" | LaForce IT Blog`} description={`Articles and guides related to ${tag}`}> <BaseLayout title={`Posts tagged with "${tag}" | LaForce IT Blog`} description={`Articles and guides related to ${tag}`}>
<main class="container"> <div class="container tag-page">
<section class="tag-header"> <header class="tag-hero">
<h1 class="tag-title">Posts tagged with <span>#{tag}</span></h1> <h1>Posts tagged with <span class="tag-highlight">{tag}</span></h1>
<p class="tag-description"> <p>Explore {sortedPosts.length} {sortedPosts.length === 1 ? 'article' : 'articles'} related to {tag}</p>
Browse all {sortedPosts.length} articles related to this topic </header>
</p>
<a href="/tags" class="tag-link">View all tags</a>
</section>
<div class="blog-grid"> <div class="posts-grid">
{sortedPosts.map((post) => ( {sortedPosts.map((post) => (
<article class="post-card"> <article class="post-card">
{/* Temporarily removed conditional image rendering for debugging */} <!-- Simplified image rendering that works reliably -->
<img <img
width={720} width={720}
height={360} height={360}
src="/images/placeholders/default.jpg" src={post.data.heroImage || "/images/placeholders/default.jpg"}
alt="" alt=""
class="post-image" class="post-image"
/> />
<div class="post-content"> <div class="post-content">
<div class="post-meta"> <time datetime={post.data.pubDate}>{formatDate(post.data.pubDate)}</time>
<time datetime={post.data.pubDate ? new Date(post.data.pubDate).toISOString() : ''}> <h2 class="post-title">
{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={`/posts/${post.slug}/`}>{post.data.title}</a> <a href={`/posts/${post.slug}/`}>{post.data.title}</a>
{post.data.draft && <span class="draft-badge">Draft</span>} </h2>
</h3>
<p class="post-excerpt">{post.data.description}</p> <p class="post-excerpt">{post.data.description}</p>
<div class="post-footer"> <div class="post-meta">
<span class="post-read-time">{post.data.readTime || '5 min read'}</span> <span class="reading-time">{post.data.minutesRead || '5 min'} read</span>
<a href={`/posts/${post.slug}/`} class="read-more">Read More</a> <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>
</div> </div>
</article> </article>
))} ))}
</div> </div>
</main>
<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">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
View all tags
</a>
</div>
</BaseLayout> </BaseLayout>
<style> <style>
.tag-header { .tag-page {
margin: 3rem 0; padding-top: 2rem;
padding-bottom: 4rem;
}
.tag-hero {
text-align: center; text-align: center;
margin-bottom: 3rem;
animation: fadeIn 0.5s ease-out;
} }
.tag-title { @keyframes fadeIn {
font-size: clamp(1.8rem, 4vw, 2.5rem); from { opacity: 0; transform: translateY(10px); }
margin-bottom: 1rem; to { opacity: 1; transform: translateY(0); }
} }
.tag-title span { .tag-hero h1 {
background: linear-gradient(90deg, var(--accent-primary), var(--accent-tertiary)); 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-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
color: transparent; color: transparent;
font-family: 'JetBrains Mono', monospace; font-weight: 700;
} }
.tag-description { .tag-hero p {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 1.1rem; font-size: var(--font-size-lg);
margin-bottom: 1rem;
} }
.tag-link { .posts-grid {
display: inline-block;
margin-top: 1rem;
color: var(--accent-primary);
text-decoration: none;
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
border-bottom: 1px dashed var(--accent-primary);
transition: all 0.3s ease;
}
.tag-link:hover {
border-bottom: 1px solid var(--accent-primary);
}
.draft-badge {
display: inline-block;
margin-left: 0.5rem;
padding: 0.25rem 0.5rem;
background-color: rgba(226, 232, 240, 0.2);
color: #94a3b8;
font-size: 0.75rem;
border-radius: 0.25rem;
vertical-align: middle;
}
/* Include styles from blog index if needed, like .blog-grid, .post-card etc. */
.blog-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 2rem; gap: 2rem;
margin: 2rem 0 4rem; margin-bottom: 3rem;
} }
.post-card { .post-card {
background: var(--card-bg); background: var(--card-bg);
border-radius: 10px; border-radius: 12px;
border: 1px solid var(--card-border);
overflow: hidden; overflow: hidden;
transition: all 0.3s ease; border: 1px solid var(--border-primary);
position: relative; transition: transform 0.3s ease, box-shadow 0.3s ease;
z-index: 1; animation: fadeIn 0.5s ease-out forwards;
animation-delay: calc(var(--animation-order, 0) * 0.1s);
opacity: 0;
display: flex;
flex-direction: column;
} }
.post-card:hover { .post-card:hover {
transform: translateY(-5px); transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(6, 182, 212, 0.1); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
border-color: rgba(56, 189, 248, 0.4); border-color: var(--accent-primary);
}
.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 { .post-image {
width: 100%; width: 100%;
height: 200px; height: 200px;
object-fit: cover; object-fit: cover;
border-bottom: 1px solid var(--card-border); border-bottom: 1px solid var(--border-primary);
} }
.post-content { .post-content {
padding: 1.5rem; padding: 1.5rem;
} flex-grow: 1;
.post-meta {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: center;
margin-bottom: 1rem;
font-size: 0.85rem;
color: var(--text-secondary);
} }
.post-category { .post-content time {
background: rgba(6, 182, 212, 0.1); color: var(--text-tertiary);
color: var(--accent-primary); font-size: var(--font-size-sm);
padding: 0.25rem 0.5rem; font-family: var(--font-mono);
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.75rem;
} }
.post-title { .post-title {
font-size: 1.25rem; font-size: var(--font-size-xl);
margin-bottom: 0.75rem; margin: 0.5rem 0 1rem;
line-height: 1.3; line-height: 1.3;
} }
.post-title a { .post-title a {
color: var(--text-primary); color: var(--text-primary);
text-decoration: none; text-decoration: none;
transition: color 0.3s ease; transition: color 0.2s ease;
} }
.post-title a:hover { .post-title a:hover {
@ -219,37 +187,83 @@ const sortedPosts = posts.sort((a, b) => {
.post-excerpt { .post-excerpt {
color: var(--text-secondary); color: var(--text-secondary);
font-size: 0.9rem; font-size: var(--font-size-md);
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
display: -webkit-box; line-height: 1.6;
-webkit-line-clamp: 3; flex-grow: 1;
-webkit-box-orient: vertical;
overflow: hidden;
} }
.post-footer { .post-meta {
display: flex; display: flex;
justify-content: space-between; flex-direction: column;
align-items: center; gap: 0.75rem;
color: var(--text-secondary); margin-top: auto;
font-size: 0.85rem;
} }
.read-more { .reading-time {
color: var(--text-tertiary);
font-size: var(--font-size-sm);
font-family: var(--font-mono);
}
.post-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
list-style: none;
padding: 0;
}
.post-tags li a {
display: block;
padding: 0.25rem 0.75rem;
background: rgba(56, 189, 248, 0.1);
border-radius: 20px;
color: var(--accent-primary); color: var(--accent-primary);
font-size: var(--font-size-xs);
text-decoration: none; text-decoration: none;
font-weight: 500; transition: all 0.2s ease;
}
.post-tags li a:hover {
background: rgba(56, 189, 248, 0.2);
transform: translateY(-2px);
}
.post-tags li a.current-tag {
background: var(--accent-primary);
color: var(--bg-primary);
}
.all-tags-link {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.25rem; gap: 0.5rem;
transition: color 0.3s ease; margin: 0 auto;
padding: 0.75rem 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;
} }
.read-more:hover { .all-tags-link:hover {
color: var(--accent-secondary); background: var(--bg-tertiary);
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
} }
.read-more::after { @media (max-width: 768px) {
content: '→'; .tag-hero h1 {
font-size: var(--font-size-2xl);
}
.posts-grid {
grid-template-columns: 1fr;
}
} }
</style> </style>

82
src/styles/theme.css Normal file
View File

@ -0,0 +1,82 @@
/* Theme Variables - Dark/Light Mode Support */
/* Dark theme (default) */
html {
/* Keep the default dark theme as defined in BaseLayout */
}
/* Light theme */
html.light-mode {
/* Primary Colors */
--bg-primary: #f8fafc;
--bg-secondary: #f1f5f9;
--bg-tertiary: #e2e8f0;
--bg-code: #f1f5f9;
--text-primary: #0f172a;
--text-secondary: #334155;
--text-tertiary: #64748b;
/* Accent Colors remain the same for brand consistency */
/* Glow Effects - lighter for light mode */
--glow-primary: rgba(6, 182, 212, 0.1);
--glow-secondary: rgba(59, 130, 246, 0.1);
--glow-tertiary: rgba(139, 92, 246, 0.1);
/* Border Colors */
--border-primary: rgba(0, 0, 0, 0.1);
--border-secondary: rgba(0, 0, 0, 0.05);
/* Card Background */
--card-bg: rgba(255, 255, 255, 0.8);
--card-border: rgba(56, 189, 248, 0.3); /* Slightly stronger border */
/* UI Element Colors */
--ui-element: #e2e8f0;
--ui-element-hover: #cbd5e1;
}
/* Background adjustments for light mode */
html.light-mode body {
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%);
}
/* Adding light mode grid overlay */
html.light-mode body::before {
background-image:
linear-gradient(rgba(15, 23, 42, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(15, 23, 42, 0.03) 1px, transparent 1px);
}
/* Theme transition for smooth switching */
html, body, * {
transition:
background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease,
box-shadow 0.3s ease;
}
/* Knowledge Graph light mode adjustments */
html.light-mode .graph-container {
background: rgba(248, 250, 252, 0.6);
}
html.light-mode .graph-loading {
background: rgba(241, 245, 249, 0.7);
}
html.light-mode .graph-filters {
background: rgba(241, 245, 249, 0.7);
}
html.light-mode .graph-legend {
background: rgba(241, 245, 249, 0.7);
}
html.light-mode .node-details {
background: rgba(248, 250, 252, 0.9);
}