upgrade to different layout
This commit is contained in:
		
							
								
								
									
										294
									
								
								src/pages/blog/[...slug].astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										294
									
								
								src/pages/blog/[...slug].astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,294 @@
 | 
			
		||||
---
 | 
			
		||||
import BlogPost from '../../layouts/BlogPost.astro';
 | 
			
		||||
 | 
			
		||||
import directus from "../../../lib/directus"
 | 
			
		||||
import { readItems } from "@directus/sdk";
 | 
			
		||||
 | 
			
		||||
export async function getStaticPaths() {
 | 
			
		||||
  const posts = await directus.request(readItems("posts", {
 | 
			
		||||
    fields: ['*'],
 | 
			
		||||
  }));
 | 
			
		||||
  
 | 
			
		||||
  const sortedEntries = [...posts].sort(
 | 
			
		||||
    (a, b) => b.published_date.valueOf() - a.published_date.valueOf()
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  return sortedEntries.map((post, index) => {
 | 
			
		||||
    return {
 | 
			
		||||
      params: { slug: post.slug },
 | 
			
		||||
      props: { 
 | 
			
		||||
        post,
 | 
			
		||||
        nextPost: index > 0 ? sortedEntries[index - 1] : null,
 | 
			
		||||
        prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const { post, nextPost, prevPost } = Astro.props;
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BlogPost slug={post.slug} title={post.title} description={post.description} content={post.content} image={post.image} image_alt={post.image_alt} published_date={post.published_date} updated_date={post.updated_date} tags={post.tags}>
 | 
			
		||||
    <!-- Main Content - Enhanced with better typography and spacing -->
 | 
			
		||||
    <div class="prose prose-zinc dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:text-zinc-800 dark:prose-a:text-zinc-300 prose-a:font-medium prose-a:underline-offset-4 hover:prose-a:text-zinc-600 dark:hover:prose-a:text-zinc-100 prose-img:rounded-xl sm:prose-base prose-sm">
 | 
			
		||||
      <div set:html={post.content} />
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- Next/Previous Navigation - Improved responsive design -->
 | 
			
		||||
    <div class="mt-12 sm:mt-16 grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 border-t border-zinc-200 dark:border-zinc-800 pt-8 sm:pt-12">
 | 
			
		||||
      {prevPost && (
 | 
			
		||||
        <a 
 | 
			
		||||
          href={`/blog/${prevPost.slug}`} 
 | 
			
		||||
          class="group relative flex flex-col h-full p-4 sm:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all duration-300 hover:-translate-y-1 overflow-hidden"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent dark:from-zinc-800 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
 | 
			
		||||
          <span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2">
 | 
			
		||||
            <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="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:-translate-x-1">
 | 
			
		||||
              <path d="m15 18-6-6 6-6"></path>
 | 
			
		||||
            </svg>
 | 
			
		||||
            Previous Article
 | 
			
		||||
          </span>
 | 
			
		||||
          <h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors">
 | 
			
		||||
            {prevPost.title}
 | 
			
		||||
          </h3>
 | 
			
		||||
        </a>
 | 
			
		||||
      )}
 | 
			
		||||
      {nextPost && (
 | 
			
		||||
        <a 
 | 
			
		||||
          href={`/blog/${nextPost.slug}`} 
 | 
			
		||||
          class="group relative flex flex-col h-full p-4 sm:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all duration-300 hover:-translate-y-1 md:text-right overflow-hidden"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent dark:from-zinc-800 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
 | 
			
		||||
          <span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2 md:justify-end">
 | 
			
		||||
            Next Article
 | 
			
		||||
            <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="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:translate-x-1">
 | 
			
		||||
              <path d="m9 18 6-6-6-6"></path>
 | 
			
		||||
            </svg>
 | 
			
		||||
          </span>
 | 
			
		||||
          <h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors">
 | 
			
		||||
            {nextPost.title}
 | 
			
		||||
          </h3>
 | 
			
		||||
        </a>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
</BlogPost>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Removing TOC-related functions
 | 
			
		||||
  
 | 
			
		||||
  // Add copy buttons to code blocks
 | 
			
		||||
  function initializeCodeCopyButtons() {
 | 
			
		||||
    const codeBlocks = document.querySelectorAll('pre');
 | 
			
		||||
    
 | 
			
		||||
    codeBlocks.forEach(block => {
 | 
			
		||||
      // Skip if already processed by either method
 | 
			
		||||
      if (block.classList.contains('code-block-processed') || block.classList.contains('enhanced')) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      block.classList.add('code-block-processed');
 | 
			
		||||
      
 | 
			
		||||
      // Create wrapper if not already wrapped
 | 
			
		||||
      let wrapper;
 | 
			
		||||
      if (block.parentNode.classList.contains('relative') && block.parentNode.classList.contains('group')) {
 | 
			
		||||
        wrapper = block.parentNode;
 | 
			
		||||
      } else {
 | 
			
		||||
        wrapper = document.createElement('div');
 | 
			
		||||
        wrapper.className = 'relative group';
 | 
			
		||||
        block.parentNode.insertBefore(wrapper, block);
 | 
			
		||||
        wrapper.appendChild(block);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Add copy button if not already present
 | 
			
		||||
      if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) {
 | 
			
		||||
        const copyButton = document.createElement('button');
 | 
			
		||||
        copyButton.className = 'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
 | 
			
		||||
        copyButton.innerHTML = `
 | 
			
		||||
          <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
 | 
			
		||||
          </svg>
 | 
			
		||||
        `;
 | 
			
		||||
        
 | 
			
		||||
        copyButton.addEventListener('click', () => {
 | 
			
		||||
          const code = block.querySelector('code').innerText;
 | 
			
		||||
          navigator.clipboard.writeText(code);
 | 
			
		||||
          
 | 
			
		||||
          // Show copied feedback
 | 
			
		||||
          copyButton.innerHTML = `
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
 | 
			
		||||
              <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
 | 
			
		||||
            </svg>
 | 
			
		||||
          `;
 | 
			
		||||
          
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            copyButton.innerHTML = `
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
 | 
			
		||||
                <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
 | 
			
		||||
              </svg>
 | 
			
		||||
            `;
 | 
			
		||||
          }, 2000);
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        wrapper.appendChild(copyButton);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Handle SPA transitions for blog post navigation
 | 
			
		||||
  function setupSPATransitions() {
 | 
			
		||||
    // Handle prev/next navigation links
 | 
			
		||||
    document.querySelectorAll('a[href^="/blog/"]').forEach(link => {
 | 
			
		||||
      // Skip links that are anchor links or already processed
 | 
			
		||||
      if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Mark as handled to avoid duplicate listeners
 | 
			
		||||
      link.setAttribute('data-spa-handled', 'true');
 | 
			
		||||
      
 | 
			
		||||
      link.addEventListener('click', (e) => {
 | 
			
		||||
        // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
 | 
			
		||||
        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const targetHref = link.getAttribute('href');
 | 
			
		||||
        
 | 
			
		||||
        // Trigger page transition animation
 | 
			
		||||
        const pageTransition = document.getElementById('page-transition');
 | 
			
		||||
        if (pageTransition) {
 | 
			
		||||
          pageTransition.classList.remove('opacity-0');
 | 
			
		||||
          pageTransition.classList.add('opacity-100');
 | 
			
		||||
          
 | 
			
		||||
          // Navigate after transition effect
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            window.location.href = targetHref;
 | 
			
		||||
          }, 300);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Fallback if transition element doesn't exist
 | 
			
		||||
          window.location.href = targetHref;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Main initialization function
 | 
			
		||||
  function initializeBlogPost() {
 | 
			
		||||
    // Initialize remaining components
 | 
			
		||||
    initializeCodeCopyButtons();
 | 
			
		||||
    setupSPATransitions();
 | 
			
		||||
    
 | 
			
		||||
    // Scroll to hash if present in URL
 | 
			
		||||
    if (window.location.hash) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        const element = document.querySelector(window.location.hash);
 | 
			
		||||
        if (element) {
 | 
			
		||||
          element.scrollIntoView({ behavior: 'smooth', block: 'start' });
 | 
			
		||||
        }
 | 
			
		||||
      }, 100);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Initialize on first load
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', initializeBlogPost);
 | 
			
		||||
  
 | 
			
		||||
  // Re-initialize when content changes via Astro's view transitions
 | 
			
		||||
  document.addEventListener('astro:page-load', initializeBlogPost);
 | 
			
		||||
  
 | 
			
		||||
  // For compatibility with custom transition system
 | 
			
		||||
  document.addEventListener('page-transition-complete', initializeBlogPost);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  /* Removing TOC-related styles */
 | 
			
		||||
  
 | 
			
		||||
  /* Language badge styling */
 | 
			
		||||
  .language-badge {
 | 
			
		||||
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
 | 
			
		||||
    text-transform: lowercase;
 | 
			
		||||
    letter-spacing: 0.05em;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Extra small screens */
 | 
			
		||||
  @media (min-width: 480px) {
 | 
			
		||||
    .xs\:inline {
 | 
			
		||||
      display: inline;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .xs\:hidden {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Enhanced typography for blog content - Responsive adjustments */
 | 
			
		||||
  .prose {
 | 
			
		||||
    @apply text-zinc-800 dark:text-zinc-200;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose h1, .prose h2, .prose h3, .prose h4 {
 | 
			
		||||
    @apply text-zinc-900 dark:text-zinc-100 font-semibold;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose h1 {
 | 
			
		||||
    @apply text-2xl sm:text-3xl md:text-4xl;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose h2 {
 | 
			
		||||
    @apply text-xl sm:text-2xl mt-8 sm:mt-12 mb-3 sm:mb-4 pb-2 border-b border-zinc-200 dark:border-zinc-800;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose h3 {
 | 
			
		||||
    @apply text-lg sm:text-xl mt-6 sm:mt-8 mb-2 sm:mb-3;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose p {
 | 
			
		||||
    @apply leading-relaxed mb-4 sm:mb-6 text-sm sm:text-base;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose a {
 | 
			
		||||
    @apply text-zinc-800 dark:text-zinc-300 font-medium underline decoration-zinc-400 dark:decoration-zinc-600 underline-offset-2 hover:text-zinc-600 dark:hover:text-zinc-100 hover:decoration-zinc-600 dark:hover:decoration-zinc-400 transition-colors;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose blockquote {
 | 
			
		||||
    @apply border-l-4 border-zinc-300 dark:border-zinc-700 pl-4 italic text-zinc-700 dark:text-zinc-300 my-4 sm:my-6;
 | 
			
		||||
  }
 | 
			
		||||
    
 | 
			
		||||
  .prose code {
 | 
			
		||||
    @apply bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 rounded text-zinc-800 dark:text-zinc-200 text-sm font-medium;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose pre {
 | 
			
		||||
    @apply bg-[#1e293b] dark:bg-[#1e293b] text-zinc-200 p-3 sm:p-4 rounded-lg overflow-x-auto text-xs sm:text-sm my-4 sm:my-6 shadow-md !important;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose pre code {
 | 
			
		||||
    @apply bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  
 | 
			
		||||
  .prose img {
 | 
			
		||||
    @apply rounded-lg shadow-md my-6 sm:my-8 mx-auto max-w-full h-auto;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose ul, .prose ol {
 | 
			
		||||
    @apply my-4 sm:my-6 pl-5 sm:pl-6;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose li {
 | 
			
		||||
    @apply mb-1 sm:mb-2 text-sm sm:text-base;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .prose hr {
 | 
			
		||||
    @apply my-8 sm:my-10 border-zinc-200 dark:border-zinc-800;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Line clamp for truncating text */
 | 
			
		||||
  .line-clamp-2 {
 | 
			
		||||
    display: -webkit-box;
 | 
			
		||||
    -webkit-line-clamp: 2;
 | 
			
		||||
    -webkit-box-orient: vertical;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										407
									
								
								src/pages/blog/index.astro
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										407
									
								
								src/pages/blog/index.astro
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,407 @@
 | 
			
		||||
---
 | 
			
		||||
import BaseLayout from '../../layouts/BaseLayout.astro';
 | 
			
		||||
 | 
			
		||||
import directus from "../../../lib/directus"
 | 
			
		||||
import { readItems } from "@directus/sdk";
 | 
			
		||||
 | 
			
		||||
const posts = await directus.request(
 | 
			
		||||
  readItems("posts", {
 | 
			
		||||
    fields: ['*'],
 | 
			
		||||
    sort: ["-published_date"],
 | 
			
		||||
  })
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const sortedPosts = posts.sort(
 | 
			
		||||
  (a, b) => b.published_date.valueOf() - a.published_date.valueOf()
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// Group posts by year for timeline effect
 | 
			
		||||
const postsByYear = sortedPosts.reduce((acc, post) => {
 | 
			
		||||
  const year = new Date(post.published_date).getFullYear();
 | 
			
		||||
  if (!acc[year]) acc[year] = [];
 | 
			
		||||
  acc[year].push(post);
 | 
			
		||||
  return acc;
 | 
			
		||||
}, {});
 | 
			
		||||
 | 
			
		||||
const years = Object.keys(postsByYear).sort((a, b) => b - a);
 | 
			
		||||
 | 
			
		||||
// Get total post count
 | 
			
		||||
const totalPosts = sortedPosts.length;
 | 
			
		||||
 | 
			
		||||
// Get unique tags for search suggestions
 | 
			
		||||
const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
<BaseLayout title="Blog">
 | 
			
		||||
  <div class="w-full max-w-6xl mx-auto px-4 sm:px-6 py-10 sm:py-16">
 | 
			
		||||
    <!-- Header with search  -->
 | 
			
		||||
    <div class="relative mb-12 sm:mb-20">
 | 
			
		||||
      <!-- Decorative elements -->
 | 
			
		||||
      <div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob"></div>
 | 
			
		||||
      <div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000"></div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="relative text-center">
 | 
			
		||||
        <h1 class="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-4">
 | 
			
		||||
          Blog
 | 
			
		||||
        </h1>
 | 
			
		||||
        
 | 
			
		||||
        <p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-10 max-w-2xl mx-auto">
 | 
			
		||||
          Thoughts, ideas, and explorations on technology and selfhosting.
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- Grid layout for mobile experience -->
 | 
			
		||||
    <div class="grid grid-cols-1 md:grid-cols-12 gap-6 sm:gap-8">
 | 
			
		||||
      <!-- Featured post (if exists) -->
 | 
			
		||||
      {sortedPosts.length > 0 && (
 | 
			
		||||
        <div class="md:col-span-12 mb-8 sm:mb-12">
 | 
			
		||||
          <article class="group relative overflow-hidden rounded-none border-b border-zinc-200 dark:border-zinc-800 pb-6 sm:pb-8">            
 | 
			
		||||
            <div class="flex flex-col md:flex-row h-full gap-6 sm:gap-8">
 | 
			
		||||
              {sortedPosts[0].image && (
 | 
			
		||||
                <div class="w-full md:w-1/2 h-60 sm:h-80 md:h-96 overflow-hidden mx-auto md:mx-0 max-w-full sm:max-w-md">
 | 
			
		||||
                  <img
 | 
			
		||||
                    src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${sortedPosts[0].image}`}
 | 
			
		||||
                    alt={sortedPosts[0].title}
 | 
			
		||||
                    class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
 | 
			
		||||
                    loading="eager"
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              )}
 | 
			
		||||
              
 | 
			
		||||
              <div class="flex-1 flex flex-col justify-center">
 | 
			
		||||
                <div class="flex items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-2 mb-3 justify-center md:justify-start">
 | 
			
		||||
                  <span class="font-medium uppercase tracking-wider">Featured</span>
 | 
			
		||||
                  <span class="h-px w-6 sm:w-8 bg-zinc-300 dark:bg-zinc-700"></span>
 | 
			
		||||
                  {sortedPosts[0].published_date && (
 | 
			
		||||
                    <time datetime={sortedPosts[0].published_date.toLocaleString()}>
 | 
			
		||||
                      {sortedPosts[0].published_date.toLocaleString('en-US', { 
 | 
			
		||||
                        year: 'numeric', 
 | 
			
		||||
                        month: 'long', 
 | 
			
		||||
                        day: 'numeric' 
 | 
			
		||||
                      })}
 | 
			
		||||
                    </time>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
                
 | 
			
		||||
                <h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center md:text-left">
 | 
			
		||||
                  <a href={`/blog/${sortedPosts[0].slug}/`} class="before:absolute before:inset-0">
 | 
			
		||||
                    {sortedPosts[0].title}
 | 
			
		||||
                  </a>
 | 
			
		||||
                </h2>
 | 
			
		||||
                
 | 
			
		||||
                <p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-4 sm:mb-6 line-clamp-3 text-center md:text-left">
 | 
			
		||||
                  {sortedPosts[0].description}
 | 
			
		||||
                </p>
 | 
			
		||||
                
 | 
			
		||||
                <!-- Improved mobile layout for featured post metadata -->
 | 
			
		||||
                <div class="flex items-center gap-3 sm:gap-4 justify-center md:justify-start flex-wrap">                  
 | 
			
		||||
                  {sortedPosts[0].tags && (
 | 
			
		||||
                    <div class="flex flex-wrap gap-2 justify-center md:justify-start">
 | 
			
		||||
                      {sortedPosts[0].tags.slice(0, 2).map((tag) => (
 | 
			
		||||
                        <span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
 | 
			
		||||
                          {tag}
 | 
			
		||||
                        </span>
 | 
			
		||||
                      ))}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </article>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      
 | 
			
		||||
      <!-- Improved sidebar for mobile -->
 | 
			
		||||
      <div class="md:col-span-3 relative">
 | 
			
		||||
        <div class="md:sticky md:top-24 space-y-4 mb-8 md:mb-0">
 | 
			
		||||
          <h3 class="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-4 uppercase tracking-wider text-center md:text-left">Archive</h3>
 | 
			
		||||
          
 | 
			
		||||
          <!-- Horizontal scrollable archive on mobile, vertical on desktop -->
 | 
			
		||||
          <div class="flex md:flex-col overflow-x-auto md:overflow-visible pb-4 md:pb-0 hide-scrollbar">
 | 
			
		||||
            {years.map((year, index) => (
 | 
			
		||||
              <a 
 | 
			
		||||
                href={`#year-${year}`}
 | 
			
		||||
                class={`flex items-center py-2 md:py-3 px-4 md:px-0 mr-3 md:mr-0 border-b border-zinc-100 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-900 transition-colors md:w-full whitespace-nowrap md:whitespace-normal rounded-full md:rounded-none ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`}
 | 
			
		||||
              >
 | 
			
		||||
                <span class="text-base md:text-lg font-medium text-zinc-900 dark:text-zinc-100">{year}</span>
 | 
			
		||||
                <span class="ml-2 md:ml-auto text-xs md:text-sm text-zinc-500 dark:text-zinc-400">
 | 
			
		||||
                  {postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
 | 
			
		||||
                </span>
 | 
			
		||||
              </a>
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <!-- Improved post grid for mobile -->
 | 
			
		||||
      <div class="md:col-span-9">
 | 
			
		||||
        {years.map((year) => (
 | 
			
		||||
          <div id={`year-${year}`} class="mb-12 sm:mb-20 scroll-mt-16">
 | 
			
		||||
            <h2 class="text-xl sm:text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 border-b border-zinc-200 dark:border-zinc-800 pb-3 sm:pb-4 text-center md:text-left">
 | 
			
		||||
              {year}
 | 
			
		||||
            </h2>
 | 
			
		||||
            
 | 
			
		||||
            <div class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}>
 | 
			
		||||
              {postsByYear[year].map((post, index) => (
 | 
			
		||||
                <article class="group relative flex flex-col h-full mx-auto md:mx-0 w-full max-w-sm sm:max-w-md">
 | 
			
		||||
                  {post.image && (
 | 
			
		||||
                    <div class="h-48 sm:h-56 overflow-hidden mb-4 rounded-lg">
 | 
			
		||||
                      <img
 | 
			
		||||
                        src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}`}
 | 
			
		||||
                        alt={post.title}
 | 
			
		||||
                        class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
 | 
			
		||||
                        loading="lazy"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  )}
 | 
			
		||||
                  
 | 
			
		||||
                  <div class="flex-1 flex flex-col">
 | 
			
		||||
                    <div class="flex items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-3 sm:gap-4 mb-2 sm:mb-3 justify-center md:justify-start flex-wrap">
 | 
			
		||||
                      {post.pubDate && (
 | 
			
		||||
                        <time datetime={post.published_date.toLocaleString()} class="flex items-center">
 | 
			
		||||
                          {post.published_date.toLocaleString('en-US', { 
 | 
			
		||||
                            month: 'short', 
 | 
			
		||||
                            day: 'numeric' 
 | 
			
		||||
                          })}
 | 
			
		||||
                        </time>
 | 
			
		||||
                      )}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <h3 class="text-lg sm:text-xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2 sm:mb-3 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center md:text-left">
 | 
			
		||||
                      <a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
 | 
			
		||||
                        {post.title}
 | 
			
		||||
                      </a>
 | 
			
		||||
                    </h3>
 | 
			
		||||
                    
 | 
			
		||||
                    <p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-2 flex-grow text-center md:text-left">
 | 
			
		||||
                      {post.description}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    
 | 
			
		||||
                    {post.tags && (
 | 
			
		||||
                      <div class="flex flex-wrap gap-2 mt-auto justify-center md:justify-start">
 | 
			
		||||
                        {post.tags.slice(0, 2).map((tag) => (
 | 
			
		||||
                          <span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
 | 
			
		||||
                            {tag}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        ))}
 | 
			
		||||
                        {post.tags.length > 2 && (
 | 
			
		||||
                          <span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
 | 
			
		||||
                            +{post.tags.length - 2}
 | 
			
		||||
                          </span>
 | 
			
		||||
                        )}
 | 
			
		||||
                      </div>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </div>
 | 
			
		||||
                </article>
 | 
			
		||||
              ))}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</BaseLayout>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
  /* Blob animation */
 | 
			
		||||
  .animate-blob {
 | 
			
		||||
    animation: blob-bounce 8s infinite ease;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .animation-delay-2000 {
 | 
			
		||||
    animation-delay: 2s;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @keyframes blob-bounce {
 | 
			
		||||
    0%, 100% {
 | 
			
		||||
      transform: translate(0, 0) scale(1);
 | 
			
		||||
    }
 | 
			
		||||
    25% {
 | 
			
		||||
      transform: translate(5%, 5%) scale(1.05);
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
      transform: translate(0, 10%) scale(1);
 | 
			
		||||
    }
 | 
			
		||||
    75% {
 | 
			
		||||
      transform: translate(-5%, 5%) scale(0.95);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Search container hover effect */
 | 
			
		||||
  .search-container:hover .search-pulse {
 | 
			
		||||
    animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  @keyframes pulse {
 | 
			
		||||
    0%, 100% {
 | 
			
		||||
      opacity: 0;
 | 
			
		||||
    }
 | 
			
		||||
    50% {
 | 
			
		||||
      opacity: 1;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Input focus animation */
 | 
			
		||||
  input:focus + div .search-pulse {
 | 
			
		||||
    animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Hide scrollbar but keep functionality */
 | 
			
		||||
  .hide-scrollbar {
 | 
			
		||||
    -ms-overflow-style: none;
 | 
			
		||||
    scrollbar-width: none;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .hide-scrollbar::-webkit-scrollbar {
 | 
			
		||||
    display: none;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Line clamp for descriptions */
 | 
			
		||||
  .line-clamp-2 {
 | 
			
		||||
    display: -webkit-box;
 | 
			
		||||
    -webkit-line-clamp: 2;
 | 
			
		||||
    -webkit-box-orient: vertical;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  .line-clamp-3 {
 | 
			
		||||
    display: -webkit-box;
 | 
			
		||||
    -webkit-line-clamp: 3;
 | 
			
		||||
    -webkit-box-orient: vertical;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /* Improved touch targets for mobile */
 | 
			
		||||
  @media (max-width: 640px) {
 | 
			
		||||
    a, button {
 | 
			
		||||
      min-height: 44px;
 | 
			
		||||
      display: flex;
 | 
			
		||||
      align-items: center;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
  // Script không thay đổi - giữ nguyên chức năng
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
    const backToTopButton = document.getElementById('back-to-top');
 | 
			
		||||
    
 | 
			
		||||
    if (backToTopButton) {
 | 
			
		||||
      // Show button when scrolled down
 | 
			
		||||
      const toggleBackToTopButton = () => {
 | 
			
		||||
        if (window.scrollY > 300) {
 | 
			
		||||
          backToTopButton.classList.remove('opacity-0', 'invisible');
 | 
			
		||||
          backToTopButton.classList.add('opacity-100', 'visible');
 | 
			
		||||
        } else {
 | 
			
		||||
          backToTopButton.classList.remove('opacity-100', 'visible');
 | 
			
		||||
          backToTopButton.classList.add('opacity-0', 'invisible');
 | 
			
		||||
        }
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      // Scroll to top when clicked
 | 
			
		||||
      backToTopButton.addEventListener('click', () => {
 | 
			
		||||
        window.scrollTo({
 | 
			
		||||
          top: 0,
 | 
			
		||||
          behavior: 'smooth'
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      // Check scroll position
 | 
			
		||||
      window.addEventListener('scroll', toggleBackToTopButton);
 | 
			
		||||
      toggleBackToTopButton(); // Initial check
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Add smooth scrolling to year links
 | 
			
		||||
    document.querySelectorAll('a[href^="#year-"]').forEach(anchor => {
 | 
			
		||||
      anchor.addEventListener('click', function (e) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const targetId = this.getAttribute('href');
 | 
			
		||||
        const targetElement = document.querySelector(targetId);
 | 
			
		||||
        
 | 
			
		||||
        if (targetElement) {
 | 
			
		||||
          window.scrollTo({
 | 
			
		||||
            top: targetElement.offsetTop - 100,
 | 
			
		||||
            behavior: 'smooth'
 | 
			
		||||
          });
 | 
			
		||||
          
 | 
			
		||||
          // Update URL hash without jumping
 | 
			
		||||
          history.pushState(null, null, targetId);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Add touch support for hover effects
 | 
			
		||||
    const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
 | 
			
		||||
    
 | 
			
		||||
    if (isTouchDevice) {
 | 
			
		||||
      const articles = document.querySelectorAll('article');
 | 
			
		||||
      
 | 
			
		||||
      articles.forEach(article => {
 | 
			
		||||
        article.addEventListener('touchstart', () => {
 | 
			
		||||
          article.classList.add('is-touched');
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        article.addEventListener('touchend', () => {
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            article.classList.remove('is-touched');
 | 
			
		||||
          }, 300);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  // SPA transition handling
 | 
			
		||||
  function setupSPATransitions() {
 | 
			
		||||
    // Handle all blog post links for SPA transitions
 | 
			
		||||
    document.querySelectorAll('a[href^="/blog/"]').forEach(link => {
 | 
			
		||||
      // Skip links that are anchor links or already processed
 | 
			
		||||
      if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Mark as handled to avoid duplicate listeners
 | 
			
		||||
      link.setAttribute('data-spa-handled', 'true');
 | 
			
		||||
      
 | 
			
		||||
      link.addEventListener('click', (e) => {
 | 
			
		||||
        // Don't handle if modifier keys are pressed (for opening in new tab, etc.)
 | 
			
		||||
        if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const targetHref = link.getAttribute('href');
 | 
			
		||||
        
 | 
			
		||||
        // Trigger page transition animation
 | 
			
		||||
        const pageTransition = document.getElementById('page-transition');
 | 
			
		||||
        if (pageTransition) {
 | 
			
		||||
          pageTransition.classList.remove('opacity-0');
 | 
			
		||||
          pageTransition.classList.add('opacity-100');
 | 
			
		||||
          
 | 
			
		||||
          // Navigate after transition effect
 | 
			
		||||
          setTimeout(() => {
 | 
			
		||||
            window.location.href = targetHref;
 | 
			
		||||
          }, 300);
 | 
			
		||||
        } else {
 | 
			
		||||
          // Fallback if transition element doesn't exist
 | 
			
		||||
          window.location.href = targetHref;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Handle year anchor links specially
 | 
			
		||||
    document.querySelectorAll('a[href^="#year-"]').forEach(anchor => {
 | 
			
		||||
      anchor.setAttribute('data-spa-internal', 'true');
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // Initialize on first load
 | 
			
		||||
  document.addEventListener('DOMContentLoaded', setupSPATransitions);
 | 
			
		||||
  
 | 
			
		||||
  // Re-initialize when content changes via Astro's view transitions
 | 
			
		||||
  document.addEventListener('astro:page-load', setupSPATransitions);
 | 
			
		||||
  
 | 
			
		||||
  // For compatibility with custom transition system
 | 
			
		||||
  document.addEventListener('page-transition-complete', setupSPATransitions);
 | 
			
		||||
</script>
 | 
			
		||||
		Reference in New Issue
	
	Block a user