feat: add blog with archives, pagination, search, sharing and RSS

- Content collection (one folder per post) with zod schema
- /blog, category and tag archives with /sayfa/<n>/ pagination
- Post page: breadcrumbs, date badges, share bar, TOC, DIY notice,
  code copy, license line, prev/next, BlogPosting + BreadcrumbList JSON-LD
- Sidebar: Pagefind search, categories, featured fallback, tag cloud
- Sätteri HAST plugins for figure/figcaption and external links
- Dual-theme Shiki, themed prose, RSS feed, latest posts on home
- Three sample posts marked as examples

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 01:06:07 +03:00
parent f2d943c31b
commit 809f89af08
27 changed files with 1599 additions and 27 deletions
+117
View File
@@ -0,0 +1,117 @@
---
import { blog } from '../../lib/data';
import {
categoryCounts,
categoryUrl,
featuredPosts,
postUrl,
tagCounts,
tagUrl,
type Post,
} from '../../lib/posts';
import SearchBox from './SearchBox.astro';
import { Icon } from 'astro-icon/components';
export interface Props {
/** All published posts (not just the current page). */
posts: Post[];
/** Highlights the current category in the list. */
activeCategory?: string;
activeTag?: string;
}
const { posts, activeCategory, activeTag } = Astro.props;
const cats = categoryCounts(posts);
const featured = featuredPosts(posts);
const tags = tagCounts(posts);
const max = Math.max(1, ...tags.map((t) => t.count));
// 4 size steps by relative frequency.
const tagSize = (count: number) =>
['text-sm', 'text-base', 'text-lg', 'text-xl'][Math.min(3, Math.floor((count / max) * 3.999))];
const box = 'border-line bg-surface border p-5';
const title = 'font-display mb-4 flex items-center gap-3 text-sm font-bold tracking-wide uppercase';
---
<aside aria-label={blog.sidebar.label} class="flex flex-col gap-6">
<section class={box} aria-labelledby="sb-search">
<h2 id="sb-search" class={title}>
{blog.sidebar.search}
<span aria-hidden="true" class="bg-accent h-0.5 w-8"></span>
</h2>
<SearchBox />
</section>
{cats.length > 0 && (
<section class={box} aria-labelledby="sb-categories">
<h2 id="sb-categories" class={title}>
{blog.sidebar.categories}
<span aria-hidden="true" class="bg-accent h-0.5 w-8"></span>
</h2>
<ul class="flex flex-col">
{cats.map(({ category, count }) => (
<li>
<a
href={categoryUrl(category.slug)}
aria-current={activeCategory === category.slug ? 'page' : undefined}
class="border-line hover:text-accent-text aria-[current=page]:text-accent-text flex items-center gap-2 border-b py-2 aria-[current=page]:font-bold"
>
<Icon name={category.icon} class="size-5 shrink-0" aria-hidden="true" />
<span class="flex-1">{category.name}</span>
<span class="font-display text-fg-muted text-sm font-bold">{count}</span>
</a>
</li>
))}
</ul>
</section>
)}
{featured.length > 0 && (
<section class={box} aria-labelledby="sb-featured">
<h2 id="sb-featured" class={title}>
{blog.sidebar.featured}
<span aria-hidden="true" class="bg-accent h-0.5 w-8"></span>
</h2>
<ol data-featured class="flex flex-col gap-4">
{featured.map((post, i) => (
<li class="flex gap-3">
<span
aria-hidden="true"
class="font-display text-accent-text text-2xl leading-none font-black"
>
{String(i + 1).padStart(2, '0')}
</span>
<a href={postUrl(post)} class="leading-snug font-semibold hover:underline">
{post.data.title}
</a>
</li>
))}
</ol>
</section>
)}
{tags.length > 0 && (
<section class={box} aria-labelledby="sb-tags">
<h2 id="sb-tags" class={title}>
{blog.sidebar.tags}
<span aria-hidden="true" class="bg-accent h-0.5 w-8"></span>
</h2>
<ul class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
{tags.map((t) => (
<li>
<a
href={tagUrl(t.tag)}
aria-current={activeTag === t.slug ? 'page' : undefined}
class:list={[
'hover:text-accent-text aria-[current=page]:text-accent-text font-semibold hover:underline',
tagSize(t.count),
]}
>
#{t.tag}
<span class="sr-only">({t.count})</span>
</a>
</li>
))}
</ul>
</section>
)}
</aside>