Merge pull request 'fresh-main' (#5) from fresh-main into main
Reviewed-on: #5
This commit is contained in:
commit
7c033218c1
|
@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
|
@ -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.
|
|
@ -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({
|
||||||
|
|
|
@ -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.
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -1,255 +1,269 @@
|
||||||
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);
|
||||||
|
|
||||||
.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;
|
opacity: 0;
|
||||||
transition: opacity 0.3s ease;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.post-card:hover::before {
|
.post-card:hover {
|
||||||
opacity: 1;
|
transform: translateY(-5px);
|
||||||
}
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
.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 {
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.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>
|
|
@ -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);
|
||||||
|
}
|
Loading…
Reference in New Issue