featmigrate from Angular to Astro static site
BREAKING CHANGE: Complete rewrite from Angular SPA to Astro - Replace Angular 18 with Astro 5.16.7 + Tailwind CSS - Convert all Angular components to Astro components - Add content collections for blog with markdown support - Setup S3 deployment with CloudFront invalidation - Add RSS feed and sitemap generation - Configure Prettier and Biome for code formatting - Switch from npm to pnpm - Remove Amplify backend (now fully static) - Improve SEO and performance with static generation
This commit is contained in:
parent
cda19e8624
commit
69d5850f5b
191 changed files with 7821 additions and 21755 deletions
347
src/pages/blog.astro
Normal file
347
src/pages/blog.astro
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { getCollection } from "astro:content";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import Navbar from "../components/Navbar.astro";
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
import type { TagCount } from "../types/i-blog";
|
||||
|
||||
type BlogPost = CollectionEntry<"blog">;
|
||||
|
||||
const allPosts: BlogPost[] = await getCollection("blog");
|
||||
const sortedPosts: BlogPost[] = allPosts
|
||||
.filter((post: BlogPost) => !post.data.draft)
|
||||
.sort((a: BlogPost, b: BlogPost) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
|
||||
|
||||
const allTags: Record<string, number> = sortedPosts.reduce(
|
||||
(acc: Record<string, number>, post: BlogPost) => {
|
||||
post.data.tags.forEach((tag: string) => {
|
||||
acc[tag] = (acc[tag] || 0) + 1;
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, number>,
|
||||
);
|
||||
|
||||
const sortedTags: TagCount[] = Object.entries(allTags)
|
||||
.sort((a: [string, number], b: [string, number]) => b[1] - a[1])
|
||||
.map(([tag, count]: [string, number]): TagCount => ({ tag, count }));
|
||||
|
||||
const initialPosts: BlogPost[] = sortedPosts.slice(0, 6);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title="Lorenzo Iovino >> Blog"
|
||||
description="Blog posts and articles by Lorenzo Iovino"
|
||||
canonicalUrl="https://www.lorenzoiovino.com/blog"
|
||||
>
|
||||
<Navbar />
|
||||
<div class="min-h-screen pt-16 bg-secondary">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 md:py-20">
|
||||
<div class="mb-16 text-center">
|
||||
<h1 class="text-white mb-4">Blog</h1>
|
||||
<p class="text-xl text-white/90 max-w-2xl mx-auto">
|
||||
Thoughts, experiences, and insights about software engineering, technology, and life
|
||||
</p>
|
||||
<div class="h-1 w-20 bg-white rounded-full mx-auto mt-6"></div>
|
||||
</div>
|
||||
|
||||
<div class="mb-12">
|
||||
<div class="bg-white rounded-2xl shadow-soft-lg overflow-hidden">
|
||||
<button
|
||||
id="toggle-tags"
|
||||
class="w-full p-6 flex items-center justify-between hover:bg-gray-50 transition-colors duration-200"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-secondary"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"
|
||||
></path>
|
||||
</svg>
|
||||
<h2 class="text-xl font-bold text-gray-900">Filter by Topic</h2>
|
||||
</div>
|
||||
<svg
|
||||
id="chevron-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-gray-400 transition-transform duration-200"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M19 9l-7 7-7-7"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="tags-container" class="hidden px-6 pb-6">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a
|
||||
href="/blog"
|
||||
class="inline-block px-4 py-2 bg-secondary text-white rounded-lg font-medium transition-all duration-200 hover:bg-secondary/90"
|
||||
>
|
||||
All Posts ({sortedPosts.length})
|
||||
</a>
|
||||
{
|
||||
sortedTags.map(({ tag, count }: TagCount) => (
|
||||
<a
|
||||
href={`/blog/tag/${tag}`}
|
||||
class="inline-block px-4 py-2 bg-gray-100 text-gray-700 rounded-lg font-medium transition-all duration-200 hover:bg-secondary hover:text-white"
|
||||
>
|
||||
{tag} ({count})
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="posts-container" class="space-y-8">
|
||||
{
|
||||
initialPosts.map((post: BlogPost) => (
|
||||
<article class="bg-white rounded-2xl shadow-soft-lg overflow-hidden hover:shadow-soft-lg transition-shadow duration-300">
|
||||
<div class="block">
|
||||
{post.data.heroImage && (
|
||||
<a href={`/blog/${post.slug}`} class="block">
|
||||
<div class="relative h-64 overflow-hidden">
|
||||
<img
|
||||
src={post.data.heroImage}
|
||||
alt={post.data.title}
|
||||
class="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
<div class="p-8">
|
||||
<div class="flex items-center gap-3 mb-4 text-sm text-gray-500">
|
||||
<time datetime={post.data.pubDate.toISOString()}>
|
||||
{post.data.pubDate.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</time>
|
||||
{post.data.tags.length > 0 && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{post.data.tags.slice(0, 3).map((tag: string) => (
|
||||
<a
|
||||
href={`/blog/tag/${tag}`}
|
||||
class="px-2 py-1 bg-secondary/10 text-secondary text-xs rounded-lg font-medium hover:bg-secondary hover:text-white transition-all duration-200"
|
||||
>
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<a href={`/blog/${post.slug}`} class="block group">
|
||||
<h2 class="text-2xl md:text-3xl font-bold text-gray-900 mb-3 group-hover:text-secondary transition-colors">
|
||||
{post.data.title}
|
||||
</h2>
|
||||
<p class="text-gray-700 text-lg leading-relaxed mb-4">
|
||||
{post.data.description}
|
||||
</p>
|
||||
<div class="flex items-center text-secondary font-medium">
|
||||
Read more
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 ml-2"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
<div id="loading" class="hidden text-center py-8">
|
||||
<div
|
||||
class="inline-block animate-spin rounded-full h-12 w-12 border-4 border-white border-t-transparent"
|
||||
>
|
||||
</div>
|
||||
<p class="text-white mt-4">Loading more posts...</p>
|
||||
</div>
|
||||
|
||||
<div id="end-message" class="hidden text-center py-8">
|
||||
<p class="text-white/80 text-lg">You've reached the end! 🎉</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</BaseLayout>
|
||||
|
||||
<script define:vars={{ allPosts: sortedPosts }}>
|
||||
let currentIndex = 6;
|
||||
const postsPerLoad = 3;
|
||||
let isLoading = false;
|
||||
|
||||
const postsContainer = document.getElementById("posts-container");
|
||||
const loading = document.getElementById("loading");
|
||||
const endMessage = document.getElementById("end-message");
|
||||
|
||||
function createPostElement(post) {
|
||||
const article = document.createElement("article");
|
||||
article.className =
|
||||
"bg-white rounded-2xl shadow-soft-lg overflow-hidden hover:shadow-soft-lg transition-shadow duration-300";
|
||||
|
||||
const heroImageHTML = post.data.heroImage
|
||||
? `
|
||||
<a href="/blog/${post.slug}" class="block">
|
||||
<div class="relative h-64 overflow-hidden">
|
||||
<img
|
||||
src="${post.data.heroImage}"
|
||||
alt="${post.data.title}"
|
||||
class="w-full h-full object-cover hover:scale-105 transition-transform duration-300"
|
||||
/>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
||||
</div>
|
||||
</a>
|
||||
`
|
||||
: "";
|
||||
|
||||
const tagsHTML =
|
||||
post.data.tags.length > 0
|
||||
? `
|
||||
<span>•</span>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
${post.data.tags
|
||||
.slice(0, 3)
|
||||
.map(
|
||||
(tag) =>
|
||||
`<a href="/blog/tag/${tag}" class="px-2 py-1 bg-secondary/10 text-secondary text-xs rounded-lg font-medium hover:bg-secondary hover:text-white transition-all duration-200">${tag}</a>`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
`
|
||||
: "";
|
||||
|
||||
const pubDate = new Date(post.data.pubDate);
|
||||
const formattedDate = pubDate.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
});
|
||||
|
||||
article.innerHTML = `
|
||||
<div class="block">
|
||||
${heroImageHTML}
|
||||
<div class="p-8">
|
||||
<div class="flex items-center gap-3 mb-4 text-sm text-gray-500">
|
||||
<time datetime="${pubDate.toISOString()}">
|
||||
${formattedDate}
|
||||
</time>
|
||||
${tagsHTML}
|
||||
</div>
|
||||
<a href="/blog/${post.slug}" class="block group">
|
||||
<h2 class="text-2xl md:text-3xl font-bold text-gray-900 mb-3 group-hover:text-secondary transition-colors">
|
||||
${post.data.title}
|
||||
</h2>
|
||||
<p class="text-gray-700 text-lg leading-relaxed mb-4">
|
||||
${post.data.description}
|
||||
</p>
|
||||
<div class="flex items-center text-secondary font-medium">
|
||||
Read more
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5 ml-2"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L12.586 11H5a1 1 0 110-2h7.586l-2.293-2.293a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
function loadMorePosts() {
|
||||
if (isLoading || currentIndex >= allPosts.length) return;
|
||||
|
||||
isLoading = true;
|
||||
loading.classList.remove("hidden");
|
||||
|
||||
setTimeout(() => {
|
||||
const nextPosts = allPosts.slice(currentIndex, currentIndex + postsPerLoad);
|
||||
|
||||
nextPosts.forEach((post) => {
|
||||
const postElement = createPostElement(post);
|
||||
postsContainer.appendChild(postElement);
|
||||
});
|
||||
|
||||
currentIndex += postsPerLoad;
|
||||
loading.classList.add("hidden");
|
||||
isLoading = false;
|
||||
|
||||
if (currentIndex >= allPosts.length) {
|
||||
endMessage.classList.remove("hidden");
|
||||
}
|
||||
}, 800);
|
||||
}
|
||||
|
||||
const toggleTagsBtn = document.getElementById("toggle-tags");
|
||||
const tagsContainer = document.getElementById("tags-container");
|
||||
const chevronIcon = document.getElementById("chevron-icon");
|
||||
|
||||
toggleTagsBtn?.addEventListener("click", () => {
|
||||
const isHidden = tagsContainer.classList.contains("hidden");
|
||||
if (isHidden) {
|
||||
tagsContainer.classList.remove("hidden");
|
||||
chevronIcon.style.transform = "rotate(180deg)";
|
||||
} else {
|
||||
tagsContainer.classList.add("hidden");
|
||||
chevronIcon.style.transform = "rotate(0deg)";
|
||||
}
|
||||
});
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && !isLoading && currentIndex < allPosts.length) {
|
||||
loadMorePosts();
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: "200px",
|
||||
}
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
observer.observe(loading);
|
||||
}
|
||||
|
||||
if (endMessage) {
|
||||
observer.observe(endMessage);
|
||||
}
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue