First Init
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
---
|
||||
import { getCollection, type CollectionEntry } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import SEO from '../../components/SEO.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await getCollection('blog', ({ data }) => !data.draft);
|
||||
return posts.map((post) => ({
|
||||
params: { slug: post.slug },
|
||||
props: { post },
|
||||
}));
|
||||
}
|
||||
|
||||
type Props = { post: CollectionEntry<'blog'> };
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { Content } = await post.render();
|
||||
|
||||
function getReadingTime(content: string): number {
|
||||
const wordsPerMinute = 200;
|
||||
const words = content.split(/\s+/).length;
|
||||
return Math.ceil(words / wordsPerMinute);
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
const readingTime = getReadingTime(post.body);
|
||||
const shareUrl = encodeURIComponent(Astro.url.href);
|
||||
const shareTitle = encodeURIComponent(post.data.title);
|
||||
const shareDescription = encodeURIComponent(post.data.description);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
ogImage={post.data.heroImage}
|
||||
>
|
||||
<SEO
|
||||
slot="head"
|
||||
type="BlogPosting"
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
breadcrumbs={[
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Blog', url: '/blog' },
|
||||
{ name: post.data.title, url: `/blog/${post.slug}` }
|
||||
]}
|
||||
article={{
|
||||
author: post.data.author.name,
|
||||
datePublished: post.data.pubDate.toISOString(),
|
||||
dateModified: post.data.updatedDate?.toISOString(),
|
||||
category: post.data.category,
|
||||
tags: post.data.tags,
|
||||
image: post.data.heroImage,
|
||||
wordCount: post.body.split(/\s+/).length
|
||||
}}
|
||||
/>
|
||||
<article class="bg-white">
|
||||
<!-- Hero Section -->
|
||||
<header class="relative bg-gradient-to-br from-secondary-900 via-secondary-800 to-secondary-900 py-16 md:py-24 overflow-hidden">
|
||||
<div class="absolute inset-0 opacity-20">
|
||||
<div class="absolute top-10 left-10 w-64 h-64 bg-primary-500 rounded-full blur-[100px]"></div>
|
||||
<div class="absolute bottom-10 right-10 w-80 h-80 bg-accent-500 rounded-full blur-[120px]"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Breadcrumb -->
|
||||
<nav class="mb-6" aria-label="Breadcrumb">
|
||||
<ol class="flex items-center gap-2 text-sm text-secondary-400">
|
||||
<li><a href="/" class="hover:text-white transition-colors">Home</a></li>
|
||||
<li><span class="mx-2">/</span></li>
|
||||
<li><a href="/blog" class="hover:text-white transition-colors">Blog</a></li>
|
||||
<li><span class="mx-2">/</span></li>
|
||||
<li class="text-secondary-300 truncate max-w-[200px]">{post.data.title}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Category -->
|
||||
<span class="inline-block px-4 py-1.5 bg-primary-500/20 text-primary-300 text-sm font-semibold rounded-full mb-4">
|
||||
{post.data.category}
|
||||
</span>
|
||||
|
||||
<!-- Title -->
|
||||
<h1 class="text-3xl md:text-4xl lg:text-5xl font-bold text-white mb-6 leading-tight">
|
||||
{post.data.title}
|
||||
</h1>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-xl text-secondary-300 mb-8 max-w-3xl">
|
||||
{post.data.description}
|
||||
</p>
|
||||
|
||||
<!-- Meta -->
|
||||
<div class="flex flex-wrap items-center gap-6 text-secondary-400">
|
||||
<!-- Author -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white font-bold">
|
||||
{post.data.author.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-white font-medium">{post.data.author.name}</p>
|
||||
<p class="text-sm">Author</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Divider -->
|
||||
<div class="hidden sm:block w-px h-10 bg-secondary-600"></div>
|
||||
|
||||
<!-- Date -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<time datetime={post.data.pubDate.toISOString()}>
|
||||
{formatDate(post.data.pubDate)}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
<!-- Reading Time -->
|
||||
<div class="flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span>{readingTime} min read</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Hero Image -->
|
||||
{post.data.heroImage && (
|
||||
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 -mt-8 relative z-10">
|
||||
<img
|
||||
src={post.data.heroImage}
|
||||
alt={post.data.title}
|
||||
width="1200"
|
||||
height="675"
|
||||
class="w-full aspect-video object-cover rounded-2xl shadow-2xl bg-secondary-200"
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<!-- Content -->
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-16">
|
||||
<div class="flex gap-8">
|
||||
<!-- Social Share Sidebar (Desktop) -->
|
||||
<aside class="hidden lg:block w-16 flex-shrink-0">
|
||||
<div class="sticky top-24 flex flex-col gap-3">
|
||||
<span class="text-xs text-secondary-400 font-medium mb-1">Share</span>
|
||||
|
||||
<!-- Twitter/X -->
|
||||
<a
|
||||
href={`https://twitter.com/intent/tweet?url=${shareUrl}&text=${shareTitle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-[#1DA1F2] text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
|
||||
aria-label="Share on Twitter"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- LinkedIn -->
|
||||
<a
|
||||
href={`https://www.linkedin.com/sharing/share-offsite/?url=${shareUrl}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-[#0A66C2] text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
|
||||
aria-label="Share on LinkedIn"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- Facebook -->
|
||||
<a
|
||||
href={`https://www.facebook.com/sharer/sharer.php?u=${shareUrl}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-[#1877F2] text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
|
||||
aria-label="Share on Facebook"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<!-- Copy Link -->
|
||||
<button
|
||||
type="button"
|
||||
id="copy-link-btn"
|
||||
class="w-10 h-10 rounded-full bg-secondary-100 hover:bg-primary-500 text-secondary-600 hover:text-white flex items-center justify-center transition-colors"
|
||||
aria-label="Copy link"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<!-- Tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-8 pb-8 border-b border-secondary-200">
|
||||
{post.data.tags.map((tag) => (
|
||||
<span class="px-3 py-1 bg-secondary-100 text-secondary-600 text-sm rounded-full hover:bg-secondary-200 transition-colors">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Article Content -->
|
||||
<div class="prose prose-lg max-w-none prose-headings:text-secondary-900 prose-headings:font-bold prose-p:text-secondary-700 prose-a:text-primary-600 prose-a:no-underline hover:prose-a:underline prose-strong:text-secondary-900 prose-code:text-primary-700 prose-code:bg-primary-50 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:font-mono prose-code:before:content-none prose-code:after:content-none prose-pre:bg-secondary-900 prose-pre:text-secondary-100 prose-pre:rounded-xl prose-pre:shadow-lg prose-img:rounded-xl prose-img:shadow-md prose-blockquote:border-primary-500 prose-blockquote:bg-primary-50 prose-blockquote:rounded-r-lg prose-blockquote:py-1 prose-blockquote:not-italic prose-li:marker:text-primary-500">
|
||||
<Content />
|
||||
</div>
|
||||
|
||||
<!-- Mobile Social Share -->
|
||||
<div class="lg:hidden mt-12 pt-8 border-t border-secondary-200">
|
||||
<p class="text-sm text-secondary-500 font-medium mb-4">Share this article</p>
|
||||
<div class="flex gap-3">
|
||||
<a
|
||||
href={`https://twitter.com/intent/tweet?url=${shareUrl}&text=${shareTitle}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex-1 py-3 rounded-lg bg-secondary-100 hover:bg-[#1DA1F2] text-secondary-600 hover:text-white flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">Twitter</span>
|
||||
</a>
|
||||
<a
|
||||
href={`https://www.linkedin.com/sharing/share-offsite/?url=${shareUrl}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex-1 py-3 rounded-lg bg-secondary-100 hover:bg-[#0A66C2] text-secondary-600 hover:text-white flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">LinkedIn</span>
|
||||
</a>
|
||||
<button
|
||||
type="button"
|
||||
id="copy-link-btn-mobile"
|
||||
class="flex-1 py-3 rounded-lg bg-secondary-100 hover:bg-primary-500 text-secondary-600 hover:text-white flex items-center justify-center gap-2 transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span class="text-sm font-medium">Copy</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Author Bio -->
|
||||
<div class="mt-12 p-6 bg-secondary-50 rounded-2xl">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-16 h-16 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white text-2xl font-bold flex-shrink-0">
|
||||
{post.data.author.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm text-secondary-500 mb-1">Written by</p>
|
||||
<h3 class="text-xl font-bold text-secondary-900 mb-2">{post.data.author.name}</h3>
|
||||
<p class="text-secondary-600">
|
||||
A passionate technologist sharing insights on modern software development, cloud architecture, and digital innovation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Back to Blog -->
|
||||
<div class="mt-8">
|
||||
<a
|
||||
href="/blog"
|
||||
class="inline-flex items-center gap-2 text-primary-600 hover:text-primary-700 font-medium transition-colors"
|
||||
>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Back to all articles
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
<style is:global>
|
||||
/* Code blocks with JetBrains Mono */
|
||||
.prose pre {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.7;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.prose pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font-family: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
.prose :not(pre) > code {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
/* Syntax highlighting colors */
|
||||
.prose pre .comment { color: #6a737d; }
|
||||
.prose pre .keyword { color: #ff7b72; }
|
||||
.prose pre .string { color: #a5d6ff; }
|
||||
.prose pre .function { color: #d2a8ff; }
|
||||
.prose pre .number { color: #79c0ff; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function copyToClipboard() {
|
||||
navigator.clipboard.writeText(window.location.href).then(() => {
|
||||
alert('Link copied to clipboard!');
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('copy-link-btn')?.addEventListener('click', copyToClipboard);
|
||||
document.getElementById('copy-link-btn-mobile')?.addEventListener('click', copyToClipboard);
|
||||
</script>
|
||||
@@ -0,0 +1,212 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||
import SEO from '../../components/SEO.astro';
|
||||
|
||||
const allPosts = await getCollection('blog', ({ data }) => !data.draft);
|
||||
const sortedPosts = allPosts.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||
|
||||
const categories = ['All', 'Web Development', 'Mobile Apps', 'AI/ML', 'Cloud', 'DevOps', 'Security'] as const;
|
||||
|
||||
function getReadingTime(content: string): number {
|
||||
const wordsPerMinute = 200;
|
||||
const words = content.split(/\s+/).length;
|
||||
return Math.ceil(words / wordsPerMinute);
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Blog"
|
||||
description="Insights and articles on web development, AI, cloud computing, and digital transformation from the WorkRoot team."
|
||||
>
|
||||
<SEO
|
||||
slot="head"
|
||||
type="WebPage"
|
||||
breadcrumbs={[
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Blog', url: '/blog' }
|
||||
]}
|
||||
/>
|
||||
<!-- Hero Section -->
|
||||
<section class="relative bg-gradient-to-br from-secondary-900 via-secondary-800 to-secondary-900 py-20 overflow-hidden">
|
||||
<div class="absolute inset-0 opacity-30">
|
||||
<div class="absolute top-20 left-10 w-72 h-72 bg-primary-500 rounded-full blur-[120px]"></div>
|
||||
<div class="absolute bottom-10 right-20 w-96 h-96 bg-accent-500 rounded-full blur-[150px]"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<h1 class="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-6">
|
||||
Our <span class="text-transparent bg-clip-text bg-gradient-to-r from-primary-400 to-accent-400">Blog</span>
|
||||
</h1>
|
||||
<p class="text-xl text-secondary-300 max-w-3xl mx-auto">
|
||||
Insights, tutorials, and industry trends from our team of experts. Stay ahead with the latest in technology.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Category Filter -->
|
||||
<section class="bg-secondary-50 border-b border-secondary-200">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div class="flex flex-wrap gap-2 justify-center" role="tablist" aria-label="Filter posts by category">
|
||||
{categories.map((category, index) => (
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={index === 0 ? 'true' : 'false'}
|
||||
data-category={category}
|
||||
class:list={[
|
||||
'filter-btn px-4 py-2 rounded-full text-sm font-medium transition-all duration-200',
|
||||
index === 0
|
||||
? 'bg-primary-500 text-white'
|
||||
: 'bg-white text-secondary-600 hover:bg-secondary-100 border border-secondary-200',
|
||||
]}
|
||||
>
|
||||
{category}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Blog Posts Grid -->
|
||||
<section class="py-16 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" id="posts-grid">
|
||||
{sortedPosts.map((post) => {
|
||||
const readingTime = getReadingTime(post.body);
|
||||
return (
|
||||
<article
|
||||
class="post-card group bg-white rounded-2xl shadow-lg overflow-hidden border border-secondary-100 hover:shadow-xl hover:border-primary-200 transition-all duration-300"
|
||||
data-category={post.data.category}
|
||||
>
|
||||
<!-- Post Image -->
|
||||
<a href={`/blog/${post.slug}`} class="block aspect-video overflow-hidden bg-secondary-200">
|
||||
{post.data.heroImage ? (
|
||||
<img
|
||||
src={post.data.heroImage}
|
||||
alt={post.data.title}
|
||||
width="640"
|
||||
height="360"
|
||||
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
) : (
|
||||
<div class="w-full h-full flex items-center justify-center bg-gradient-to-br from-primary-500 to-primary-700">
|
||||
<svg class="w-16 h-16 text-white/50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
|
||||
<!-- Post Content -->
|
||||
<div class="p-6">
|
||||
<!-- Category & Reading Time -->
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<span class="px-3 py-1 bg-primary-50 text-primary-700 text-xs font-semibold rounded-full">
|
||||
{post.data.category}
|
||||
</span>
|
||||
<span class="text-secondary-400 text-sm flex items-center gap-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
{readingTime} min read
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Title -->
|
||||
<h2 class="text-xl font-bold text-secondary-900 mb-3 line-clamp-2 group-hover:text-primary-600 transition-colors">
|
||||
<a href={`/blog/${post.slug}`}>{post.data.title}</a>
|
||||
</h2>
|
||||
|
||||
<!-- Description -->
|
||||
<p class="text-secondary-600 text-sm mb-4 line-clamp-3">
|
||||
{post.data.description}
|
||||
</p>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
{post.data.tags.slice(0, 3).map((tag) => (
|
||||
<span class="text-xs text-secondary-500 bg-secondary-100 px-2 py-1 rounded">
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Author & Date -->
|
||||
<div class="flex items-center justify-between pt-4 border-t border-secondary-100">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-primary-400 to-primary-600 flex items-center justify-center text-white text-sm font-bold">
|
||||
{post.data.author.name.charAt(0)}
|
||||
</div>
|
||||
<span class="text-sm text-secondary-600">{post.data.author.name}</span>
|
||||
</div>
|
||||
<time class="text-sm text-secondary-400" datetime={post.data.pubDate.toISOString()}>
|
||||
{formatDate(post.data.pubDate)}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div id="empty-state" class="hidden text-center py-16">
|
||||
<svg class="w-16 h-16 text-secondary-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2z" />
|
||||
</svg>
|
||||
<h3 class="text-xl font-semibold text-secondary-600 mb-2">No posts found</h3>
|
||||
<p class="text-secondary-400">Try selecting a different category.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
const filterButtons = document.querySelectorAll('.filter-btn');
|
||||
const postCards = document.querySelectorAll('.post-card');
|
||||
const emptyState = document.getElementById('empty-state');
|
||||
|
||||
filterButtons.forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const category = btn.getAttribute('data-category');
|
||||
|
||||
// Update button states
|
||||
filterButtons.forEach((b) => {
|
||||
b.classList.remove('bg-primary-500', 'text-white');
|
||||
b.classList.add('bg-white', 'text-secondary-600', 'border', 'border-secondary-200');
|
||||
b.setAttribute('aria-selected', 'false');
|
||||
});
|
||||
btn.classList.remove('bg-white', 'text-secondary-600', 'border', 'border-secondary-200');
|
||||
btn.classList.add('bg-primary-500', 'text-white');
|
||||
btn.setAttribute('aria-selected', 'true');
|
||||
|
||||
// Filter posts
|
||||
let visibleCount = 0;
|
||||
postCards.forEach((card) => {
|
||||
const postCategory = card.getAttribute('data-category');
|
||||
if (category === 'All' || postCategory === category) {
|
||||
card.classList.remove('hidden');
|
||||
visibleCount++;
|
||||
} else {
|
||||
card.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Show empty state if no posts
|
||||
if (emptyState) {
|
||||
emptyState.classList.toggle('hidden', visibleCount > 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user