The questions hikers ask before they buy, and where AI answers send them.
Research
Blog card with an inset image, byline, title and arrow, excerpt, tag and date. The whole card is a single link.
ContentComponentNo dependencies Original, MIT
The questions hikers ask before they buy, and where AI answers send them.
Research
Using Claude Code? Give it the agent prompt and skip these steps.
foundation.css holds the tokens every component reads.
---
// src/layouts/Layout.astro
import "../styles/foundation.css";
--- ---
import PostCard from "../components/viewership/post-card/PostCard.astro";
---
<PostCard /> | Prop | Type | Default | Description |
|---|---|---|---|
href | string | · | Post URL. The title link is stretched over the whole card. |
title | string | · | Post title. |
excerpt | string | · | One or two sentences under the title. |
author | string | · | Byline name. |
readTime | string | · | Shown after the author, e.g. "5 min read". |
tag | string | · | Category pill in the footer. |
date | string | Date | · | ISO date or Date, rendered in a <time> element. |
image | { src; alt; width?; height? } | · | Cover image, cropped to 3:2. |
as | "h2" | "h3" | "h4" | "h3" | Heading element for the title. |
slot: media | slot | · | Replaces the image (video, illustration). |
---
/**
* PostCard: a blog or journal card. Inset image, a byline, the title with an
* arrow, a short excerpt, then a tag and the date. The whole card is one link
* (the title's link is stretched over it), so screen readers hear one name.
*
* @example
* <PostCard href="/journal/waxing" title="How to rewax a canvas pack" excerpt="Ten minutes, one bar of wax." author="Field team" readTime="5 min read" tag="Repairs" date="2026-03-09" image={{ src: "/pack.webp", alt: "" }} />
*/
interface Image {
src: string;
alt: string;
width?: number;
height?: number;
}
interface Props {
href: string;
title: string;
excerpt?: string;
author?: string;
readTime?: string;
tag?: string;
/** ISO date (2026-03-09) or a Date. Rendered in a <time>. */
date?: string | Date;
image?: Image;
as?: "h2" | "h3" | "h4";
class?: string;
}
const { href, title, excerpt, author, readTime, tag, date, image, as: Heading = "h3", class: className } = Astro.props;
const d = date ? (date instanceof Date ? date : new Date(`${date}T00:00:00`)) : null;
const iso = d ? d.toISOString().slice(0, 10) : undefined;
const pretty = d ? d.toLocaleDateString("en-US", { month: "short", day: "2-digit", year: "numeric" }) : undefined;
const hasMedia = image || Astro.slots.has("media");
---
<article class:list={["vs-post-card", className]}>
{
hasMedia && (
<div class="vs-post-card__media">
<slot name="media">{image && <img src={image.src} alt={image.alt} width={image.width} height={image.height} loading="lazy" />}</slot>
</div>
)
}
{
(author || readTime) && (
<p class="vs-post-card__byline">
{author && <span>{author}</span>}
{author && readTime && <span class="vs-post-card__dot" aria-hidden="true" />}
{readTime && <span>{readTime}</span>}
</p>
)
}
<div class="vs-post-card__head">
<Heading class="vs-post-card__title">
<a href={href}>{title}</a>
</Heading>
<svg class="vs-post-card__arrow" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 17 17 7M7 7h10v10" /></svg>
</div>
{excerpt && <p class="vs-post-card__excerpt">{excerpt}</p>}
{
(tag || d) && (
<p class="vs-post-card__foot">
{tag && <span class="vs-post-card__tag">{tag}</span>}
{d && <time datetime={iso}>{pretty}</time>}
</p>
)
}
</article>
<style>
.vs-post-card {
position: relative;
display: flex;
flex-direction: column;
padding: 14px 14px 20px;
border-radius: calc(var(--vs-radius-window) + 6px);
background: var(--vs-surface);
color: var(--vs-ink);
font-family: var(--vs-font-sans);
font-stretch: var(--vs-font-stretch);
border: var(--vs-border-w) solid var(--vs-elev-ring);
box-shadow: var(--vs-pop-shadow, var(--vs-shadow-sm));
transition:
transform var(--vs-duration) var(--vs-ease-out),
box-shadow var(--vs-duration) var(--vs-ease-out);
}
.vs-post-card:hover {
transform: translateY(-3px);
box-shadow: var(--vs-pop-shadow, var(--vs-shadow-lg));
}
.vs-post-card:focus-within {
box-shadow: var(--vs-focus-ring);
}
.vs-post-card__media {
aspect-ratio: 3 / 2;
overflow: hidden;
border-radius: var(--vs-radius-window);
background: var(--vs-canvas);
}
.vs-post-card__media :global(:is(img, video)) {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 600ms var(--vs-ease-out);
}
.vs-post-card:hover .vs-post-card__media :global(:is(img, video)) {
transform: scale(1.03);
}
.vs-post-card__byline {
display: flex;
align-items: center;
gap: 10px;
margin: 18px 6px 0;
font-size: 0.875rem;
color: var(--vs-ink-3);
}
.vs-post-card__dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: currentColor;
opacity: 0.6;
}
.vs-post-card__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin: 10px 6px 0;
}
.vs-post-card__title {
margin: 0;
font-size: 1.1875rem;
font-weight: 500;
line-height: 1.35;
letter-spacing: -0.01em;
text-wrap: pretty;
}
.vs-post-card__title a {
color: inherit;
text-decoration: none;
outline: none;
}
/* One link, the whole card. */
.vs-post-card__title a::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
}
.vs-post-card__arrow {
flex: none;
width: 22px;
height: 22px;
margin-top: 2px;
fill: none;
stroke: var(--vs-ink-3);
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
transition:
transform var(--vs-duration) var(--vs-ease-out),
stroke var(--vs-duration-fast) ease-out;
}
.vs-post-card:hover .vs-post-card__arrow {
stroke: var(--vs-accent);
transform: translate(2px, -2px);
}
.vs-post-card__excerpt {
margin: 8px 6px 0;
font-size: 1rem;
line-height: 1.5;
color: var(--vs-ink-2);
text-wrap: pretty;
}
.vs-post-card__foot {
display: flex;
align-items: center;
gap: 14px;
margin: auto 6px 0;
padding-top: 18px;
font-size: 0.875rem;
color: var(--vs-ink-3);
}
.vs-post-card__tag {
padding: 6px 14px;
border-radius: var(--vs-radius-pill);
background: var(--vs-inset);
border: var(--vs-border-w) solid var(--vs-line-soft);
color: var(--vs-ink);
}
</style> Original, MIT.
Used by Blog Grid