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:
@@ -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>
|
||||
Reference in New Issue
Block a user