Simple 01
A centred header with search, category tabs and a sort menu, a grid of posts with a newsletter card inside it, and pagination.
Last updated
---
/**
* Simple 01: A centred header with search, category tabs and a sort menu, a grid of posts with a newsletter card inside it, and pagination.
*
* @block blog
* @order 5
*/
import { Search as SearchIcon } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import SectionHeader from '@/components/apogee/SectionHeader/SectionHeader.astro'
import PostCard from '@/components/apogee/PostCard/PostCard.astro'
import { plain } from '@/components/apogee/sections/_shared/spine.ts'
import { toCard, type BlogContent, type Post } from '@/components/apogee/sections/_shared/blog.ts'
import Grid from '@/components/apogee/Grid/Grid.astro'
import Tabs from '@/components/apogee/Tabs/Tabs.astro'
import Select from '@/components/apogee/Select/Select.astro'
import Pagination from '@/components/apogee/Pagination/Pagination.astro'
import Input from '@/components/apogee/Input/Input.astro'
import Button from '@/components/apogee/Button/Button.astro'
import Link from '@/components/apogee/Link/Link.astro'
import Card from '@/components/apogee/Card/Card.astro'
const demoPosts: Post[] = [
{
title: 'Designing review sessions people look forward to',
href: '#',
excerpt: 'How to run critiques that sharpen the work instead of draining the room.',
date: '2026-01-20',
readingTime: '8 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-1/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Design', href: '#' },
author: { name: 'Ada Lovelace', href: '#' },
tags: [{ name: 'Design', href: '#', color: 'primary' }, { name: 'Research', href: '#', color: 'neutral' }, { name: 'Process', href: '#', color: 'positive' }],
},
{
title: 'Migrating to a typed component library',
href: '#',
excerpt: 'What changed when we moved two hundred screens onto one set of typed parts.',
date: '2026-01-19',
readingTime: '6 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-2/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Engineering', href: '#' },
author: { name: 'Alan Turing', href: '#' },
tags: [{ name: 'Engineering', href: '#', color: 'primary' }, { name: 'Tooling', href: '#', color: 'neutral' }],
},
{
title: 'Building your first internal API',
href: '#',
excerpt: 'The shape of a small API that teams can grow without rewriting.',
date: '2026-01-18',
readingTime: '7 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-3/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Engineering', href: '#' },
author: { name: 'Grace Hopper', href: '#' },
tags: [{ name: 'Engineering', href: '#', color: 'primary' }, { name: 'APIs', href: '#', color: 'warning' }],
},
{
title: 'A field guide to product discovery',
href: '#',
excerpt: 'Questions that turn a vague request into something you can build.',
date: '2026-01-16',
readingTime: '5 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-4/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Product', href: '#' },
author: { name: 'Katherine Johnson', href: '#' },
tags: [{ name: 'Product', href: '#', color: 'primary' }, { name: 'Research', href: '#', color: 'neutral' }],
},
{
title: 'Writing release notes people read',
href: '#',
excerpt: 'Lead with what changed for the reader, not what changed in the code.',
date: '2026-01-15',
readingTime: '4 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-5/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Product', href: '#' },
author: { name: 'Margaret Hamilton', href: '#' },
tags: [{ name: 'Product', href: '#', color: 'primary' }, { name: 'Writing', href: '#', color: 'positive' }],
},
{
title: 'Leadership lessons from a year of remote teams',
href: '#',
excerpt: 'Rituals, writing and trust: what held a distributed team together.',
date: '2026-01-14',
readingTime: '9 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-6/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Leadership', href: '#' },
author: { name: 'Alan Kay', href: '#' },
tags: [{ name: 'Leadership', href: '#', color: 'primary' }, { name: 'Remote', href: '#', color: 'neutral' }],
},
{
title: 'Accessible colour, measured',
href: '#',
excerpt: 'Why our build fails when a colour pair misses AA, and how we fixed the misses.',
date: '2026-01-12',
readingTime: '6 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-7/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Design', href: '#' },
author: { name: 'Hedy Lamarr', href: '#' },
tags: [{ name: 'Design', href: '#', color: 'primary' }, { name: 'Accessibility', href: '#', color: 'positive' }],
},
{
title: 'Shipping on Fridays without fear',
href: '#',
excerpt: 'Small changes, feature flags and a rollback you have practised.',
date: '2026-01-10',
readingTime: '5 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-8/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Engineering', href: '#' },
author: { name: 'Dennis Ritchie', href: '#' },
tags: [{ name: 'Engineering', href: '#', color: 'primary' }, { name: 'Delivery', href: '#', color: 'warning' }],
},
]
const demoCategories = [
{ label: 'View all', value: 'all', href: '#' },
{ label: 'Design', value: 'design', href: '#design' },
{ label: 'Product', value: 'product', href: '#product' },
{ label: 'Engineering', value: 'engineering', href: '#engineering' },
{ label: 'Leadership', value: 'leadership', href: '#leadership' },
{ label: 'Marketing', value: 'marketing', href: '#marketing' },
]
/** @composable */
interface Props extends BlogContent {
/** The newsletter card in the grid. @category content */
formHeading?: string
/** @category content */
formDescription?: string | null
/** The sign-up form's submit button. @category content */
submitLabel?: string
}
const {
eyebrow = 'Our blog',
heading = 'The latest writings from our team',
description = 'The latest industry news, interviews, technologies and resources.',
items = demoPosts,
locale = Astro.currentLocale ?? 'en',
categories = demoCategories,
formHeading = 'Weekly newsletter',
formDescription = 'No spam. Just the latest releases and tips, interesting articles, and interviews in your inbox every week.',
submitLabel = 'Subscribe',
} = Astro.props
---
<Section spacing="large">
<SectionHeader eyebrow={eyebrow ?? undefined} heading={plain(heading)} description={description ?? undefined} align="center" headingLevel={1} class="mb-8" />
<div class="mx-auto mb-16 max-w-sm"><Input type="search" name="q" placeholder="Search" aria-label="Search posts"><SearchIcon slot="start" /></Input></div>
<div class="mb-10 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
{categories && <Tabs label="Categories" value={categories[0]?.value} tabs={categories} class="min-w-0" />}
<Select aria-label="Sort by" class="sm:w-48">
<option>Most recent</option>
<option>Most popular</option>
<option>Oldest</option>
</Select>
</div>
<Grid as="ul" cols={{ base: 1, md: 2, lg: 3 }} gap={8} class="list-none p-0">
{items.slice(0, 3).map((post) => <li><PostCard post={toCard(post)} locale={locale} headingLevel={2} class="h-full" /></li>)}
<li>
<Card padding="large" class="h-full bg-neutral-faded">
<h2 class="text-xl font-semibold">{formHeading}</h2>
{formDescription && <p class="mt-2 text-neutral-faded">{formDescription}</p>}
<div class="mt-6"><form method="post" class="flex w-full flex-col gap-2 sm:max-w-md">
<div class="flex flex-col gap-3 sm:flex-row">
<Input type="email" name="email" placeholder="Enter your email" aria-label="Email" autocomplete="email" required class="flex-1" />
<Button type="submit">{submitLabel}</Button>
</div>
<p class="text-sm opacity-85">We care about your data in our <Link href="#" color="neutral">privacy policy</Link>.</p>
</form></div>
</Card>
</li>
{items.slice(3).map((post) => <li><PostCard post={toCard(post)} locale={locale} headingLevel={2} class="h-full" /></li>)}
</Grid>
<Pagination currentPage={1} totalPages={10} getHref={(p) => `?page=${p}`} labels class="mt-12 justify-between border-t border-neutral pt-5" />
</Section>
Show codeHide code
---
/**
* Simple 01: A centred header with search, category tabs and a sort menu, a grid of posts with a newsletter card inside it, and pagination.
*
* @block blog
* @order 5
*/
import { Search as SearchIcon } from '@lucide/astro'
import Section from '@/components/apogee/Section/Section.astro'
import SectionHeader from '@/components/apogee/SectionHeader/SectionHeader.astro'
import PostCard from '@/components/apogee/PostCard/PostCard.astro'
import { plain } from '@/components/apogee/sections/_shared/spine.ts'
import { toCard, type BlogContent, type Post } from '@/components/apogee/sections/_shared/blog.ts'
import Grid from '@/components/apogee/Grid/Grid.astro'
import Tabs from '@/components/apogee/Tabs/Tabs.astro'
import Select from '@/components/apogee/Select/Select.astro'
import Pagination from '@/components/apogee/Pagination/Pagination.astro'
import Input from '@/components/apogee/Input/Input.astro'
import Button from '@/components/apogee/Button/Button.astro'
import Link from '@/components/apogee/Link/Link.astro'
import Card from '@/components/apogee/Card/Card.astro'
const demoPosts: Post[] = [
{
title: 'Designing review sessions people look forward to',
href: '#',
excerpt: 'How to run critiques that sharpen the work instead of draining the room.',
date: '2026-01-20',
readingTime: '8 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-1/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Design', href: '#' },
author: { name: 'Ada Lovelace', href: '#' },
tags: [{ name: 'Design', href: '#', color: 'primary' }, { name: 'Research', href: '#', color: 'neutral' }, { name: 'Process', href: '#', color: 'positive' }],
},
{
title: 'Migrating to a typed component library',
href: '#',
excerpt: 'What changed when we moved two hundred screens onto one set of typed parts.',
date: '2026-01-19',
readingTime: '6 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-2/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Engineering', href: '#' },
author: { name: 'Alan Turing', href: '#' },
tags: [{ name: 'Engineering', href: '#', color: 'primary' }, { name: 'Tooling', href: '#', color: 'neutral' }],
},
{
title: 'Building your first internal API',
href: '#',
excerpt: 'The shape of a small API that teams can grow without rewriting.',
date: '2026-01-18',
readingTime: '7 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-3/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Engineering', href: '#' },
author: { name: 'Grace Hopper', href: '#' },
tags: [{ name: 'Engineering', href: '#', color: 'primary' }, { name: 'APIs', href: '#', color: 'warning' }],
},
{
title: 'A field guide to product discovery',
href: '#',
excerpt: 'Questions that turn a vague request into something you can build.',
date: '2026-01-16',
readingTime: '5 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-4/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Product', href: '#' },
author: { name: 'Katherine Johnson', href: '#' },
tags: [{ name: 'Product', href: '#', color: 'primary' }, { name: 'Research', href: '#', color: 'neutral' }],
},
{
title: 'Writing release notes people read',
href: '#',
excerpt: 'Lead with what changed for the reader, not what changed in the code.',
date: '2026-01-15',
readingTime: '4 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-5/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Product', href: '#' },
author: { name: 'Margaret Hamilton', href: '#' },
tags: [{ name: 'Product', href: '#', color: 'primary' }, { name: 'Writing', href: '#', color: 'positive' }],
},
{
title: 'Leadership lessons from a year of remote teams',
href: '#',
excerpt: 'Rituals, writing and trust: what held a distributed team together.',
date: '2026-01-14',
readingTime: '9 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-6/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Leadership', href: '#' },
author: { name: 'Alan Kay', href: '#' },
tags: [{ name: 'Leadership', href: '#', color: 'primary' }, { name: 'Remote', href: '#', color: 'neutral' }],
},
{
title: 'Accessible colour, measured',
href: '#',
excerpt: 'Why our build fails when a colour pair misses AA, and how we fixed the misses.',
date: '2026-01-12',
readingTime: '6 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-7/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Design', href: '#' },
author: { name: 'Hedy Lamarr', href: '#' },
tags: [{ name: 'Design', href: '#', color: 'primary' }, { name: 'Accessibility', href: '#', color: 'positive' }],
},
{
title: 'Shipping on Fridays without fear',
href: '#',
excerpt: 'Small changes, feature flags and a rollback you have practised.',
date: '2026-01-10',
readingTime: '5 min read',
image: { src: 'https://picsum.photos/seed/apogee-blog-8/960/640', alt: '', width: 960, height: 640 },
category: { name: 'Engineering', href: '#' },
author: { name: 'Dennis Ritchie', href: '#' },
tags: [{ name: 'Engineering', href: '#', color: 'primary' }, { name: 'Delivery', href: '#', color: 'warning' }],
},
]
const demoCategories = [
{ label: 'View all', value: 'all', href: '#' },
{ label: 'Design', value: 'design', href: '#design' },
{ label: 'Product', value: 'product', href: '#product' },
{ label: 'Engineering', value: 'engineering', href: '#engineering' },
{ label: 'Leadership', value: 'leadership', href: '#leadership' },
{ label: 'Marketing', value: 'marketing', href: '#marketing' },
]
/** @composable */
interface Props extends BlogContent {
/** The newsletter card in the grid. @category content */
formHeading?: string
/** @category content */
formDescription?: string | null
/** The sign-up form's submit button. @category content */
submitLabel?: string
}
const {
eyebrow = 'Our blog',
heading = 'The latest writings from our team',
description = 'The latest industry news, interviews, technologies and resources.',
items = demoPosts,
locale = Astro.currentLocale ?? 'en',
categories = demoCategories,
formHeading = 'Weekly newsletter',
formDescription = 'No spam. Just the latest releases and tips, interesting articles, and interviews in your inbox every week.',
submitLabel = 'Subscribe',
} = Astro.props
---
<Section spacing="large">
<SectionHeader eyebrow={eyebrow ?? undefined} heading={plain(heading)} description={description ?? undefined} align="center" headingLevel={1} class="mb-8" />
<div class="mx-auto mb-16 max-w-sm"><Input type="search" name="q" placeholder="Search" aria-label="Search posts"><SearchIcon slot="start" /></Input></div>
<div class="mb-10 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
{categories && <Tabs label="Categories" value={categories[0]?.value} tabs={categories} class="min-w-0" />}
<Select aria-label="Sort by" class="sm:w-48">
<option>Most recent</option>
<option>Most popular</option>
<option>Oldest</option>
</Select>
</div>
<Grid as="ul" cols={{ base: 1, md: 2, lg: 3 }} gap={8} class="list-none p-0">
{items.slice(0, 3).map((post) => <li><PostCard post={toCard(post)} locale={locale} headingLevel={2} class="h-full" /></li>)}
<li>
<Card padding="large" class="h-full bg-neutral-faded">
<h2 class="text-xl font-semibold">{formHeading}</h2>
{formDescription && <p class="mt-2 text-neutral-faded">{formDescription}</p>}
<div class="mt-6"><form method="post" class="flex w-full flex-col gap-2 sm:max-w-md">
<div class="flex flex-col gap-3 sm:flex-row">
<Input type="email" name="email" placeholder="Enter your email" aria-label="Email" autocomplete="email" required class="flex-1" />
<Button type="submit">{submitLabel}</Button>
</div>
<p class="text-sm opacity-85">We care about your data in our <Link href="#" color="neutral">privacy policy</Link>.</p>
</form></div>
</Card>
</li>
{items.slice(3).map((post) => <li><PostCard post={toCard(post)} locale={locale} headingLevel={2} class="h-full" /></li>)}
</Grid>
<Pagination currentPage={1} totalPages={10} getHref={(p) => `?page=${p}`} labels class="mt-12 justify-between border-t border-neutral pt-5" />
</Section>Add
npx @ava-ui/apogee add blog/simple-01
Every piece of copy is a prop, with the sample copy as its default. Pass yours, or null to hide a part; see copy is props.
This copies the block to src/components/apogee/blocks/blog/ with everything it uses: Button, Card, Grid, Input, Link, Pagination, PostCard, Section, SectionHeader, Select, Tabs. It opens a blog index page (its heading is the page’s h1). Swap the posts array for your content collection. Use Copy code above to paste it instead.
Accessibility
- Each post is one link: the title stretches over the card, and the author, category and tag links sit above it and stay clickable.
- The decorative arrow and the images are hidden from screen readers (their alt is empty because the title already names the post).
- The category row is a
<nav>named “Categories”; the current category carriesaria-current="page". - The email field is named by
aria-label="Email"(the placeholder is not a label); the form submits without JS. - The search field is named “Search posts”.
- Pagination is a
<nav>of links; the current page carriesaria-current="page".