A centred header with an email form, then one centred column of horizontal posts and pagination.

Last updated

Open block
Show code
---
/**
 * Alternative layout 04: A centred header with an email form, then one centred column of horizontal posts and pagination.
 *
 * @block blog
 * @order 16
 */
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 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'

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' }],
  },
]
/** @composable */
interface Props extends BlogContent {
  /** The sign-up form's submit button. @category content */
  submitLabel?: string
}

const {
  eyebrow = 'Our blog',
  heading = 'Stories and interviews',
  description = 'Interviews, tips, guides, industry best practices, and news.',
  items = demoPosts,
  locale = Astro.currentLocale ?? 'en',
  submitLabel = 'Subscribe',
} = Astro.props

---

<Section spacing="large" containerWidth="medium">
  <SectionHeader eyebrow={eyebrow ?? undefined} heading={plain(heading)} description={description ?? undefined} align="center" headingLevel={1} class="mb-8" />
  <div class="mb-16 flex justify-center"><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>
  <ul class="flex list-none flex-col gap-12 p-0">
    {items.map((post) => <li><PostCard post={toCard(post)} locale={locale} orientation="horizontal" meta="none" headingLevel={2} /></li>)}
  </ul>
  <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/alt-layout-04

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, Input, Link, Pagination, PostCard, Section, SectionHeader. 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 email field is named by aria-label="Email" (the placeholder is not a label); the form submits without JS.
  • Pagination is a <nav> of links; the current page carries aria-current="page".