Initial commit: Astro + EmDash CMS starter

This commit is contained in:
Kunthawat Greethong
2026-04-29 06:40:38 +07:00
commit a6d4eebd71
27 changed files with 15807 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
---
import {
getTerm,
getEmDashCollection,
getTermsForEntries,
decodeSlug,
} from "emdash";
import Base from "../../layouts/Base.astro";
import PostCard from "../../components/PostCard.astro";
import { getReadingTime } from "../../utils/reading-time";
const slug = decodeSlug(Astro.params.slug);
const term = slug ? await getTerm("category", slug) : null;
if (!term) {
return Astro.redirect("/404");
}
const { entries: posts, cacheHint } = await getEmDashCollection("posts", {
where: { category: term.slug },
orderBy: { published_at: "desc" },
});
Astro.cache.set(cacheHint);
// Single batched query for tags on every post in this category, rather
// than calling getEntryTerms() per post (which would be one round-trip
// per post).
const tagsByEntry = await getTermsForEntries(
"posts",
posts.map((p) => p.data.id),
"tag",
);
const filteredPosts = posts.map((post) => ({
post,
tags: tagsByEntry.get(post.data.id) ?? [],
}));
---
<Base title={`${term.label} posts`} description={`All posts in ${term.label}`}>
<section class="archive-section">
<header class="archive-header">
<span class="archive-label">Category</span>
<h1 class="archive-title">{term.label}</h1>
<p class="archive-count">
{filteredPosts.length}
{filteredPosts.length === 1 ? "post" : "posts"}
</p>
</header>
{
filteredPosts.length === 0 ? (
<p class="no-posts">No posts in this category yet.</p>
) : (
<div class="posts-grid">
{filteredPosts.map(({ post, tags }) => (
<PostCard
title={post.data.title}
excerpt={post.data.excerpt}
featuredImage={post.data.featured_image}
href={`/posts/${post.id}`}
date={post.data.publishedAt ?? undefined}
readingTime={getReadingTime(post.data.content)}
tags={tags.map((t) => ({ slug: t.slug, label: t.label }))}
/>
))}
</div>
)
}
</section>
</Base>
<style>
.archive-section {
max-width: var(--wide-width);
margin: 0 auto;
padding: var(--spacing-12) var(--spacing-6);
}
.archive-header {
margin-bottom: var(--spacing-12);
padding-bottom: var(--spacing-8);
border-bottom: 1px solid var(--color-border-subtle);
}
.archive-label {
display: block;
font-size: var(--font-size-xs);
font-weight: 500;
color: var(--color-accent);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
margin-bottom: var(--spacing-2);
}
.archive-title {
font-size: var(--font-size-4xl);
font-weight: 700;
letter-spacing: var(--tracking-tight);
margin-bottom: var(--spacing-2);
}
.archive-count {
font-size: var(--font-size-sm);
color: var(--color-muted);
}
.posts-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--spacing-12) var(--spacing-8);
}
.no-posts {
color: var(--color-muted);
}
@media (max-width: 900px) {
.posts-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.posts-grid {
grid-template-columns: 1fr;
}
}
</style>