northwind.test
Field notes, monthly.
Trip reports, repair tips and first look at new batches. One email a month.
Email sign-up with a short pitch and privacy line. A plain HTML form you point at any provider.
ConversionSectionNo dependencies Original, MIT
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 Newsletter from "../components/viewership/newsletter/Newsletter.astro";
---
<Newsletter /> | Prop | Type | Default | Description |
|---|---|---|---|
title | string | · | Headline. |
accent | string | · | Closing words of the headline. |
body | string | · | Short pitch. |
action | string | · | Form endpoint (your email provider or a serverless function). |
method | "post" | "get" | "post" | Form method. |
field | string | "email" | Name of the email field. |
placeholder | string | "you@example.com" | Input placeholder. |
button | string | "Subscribe" | Button label. |
note | string | · | Small print under the form. |
as | "h2" | "h3" | "h2" | Heading element. |
---
/**
* Newsletter: an email sign-up with a short pitch and a privacy line.
*
* A plain HTML form: point `action` at your provider (Buttondown, ConvertKit,
* a Netlify or Vercel function) and set `field` to the name it expects. No
* JavaScript; the browser validates the email.
*
* @example
* <Newsletter title="Field notes," accent="monthly." action="https://example.com/subscribe" />
*/
interface Props {
title: string;
accent?: string;
body?: string;
/** Form endpoint. */
action: string;
method?: "post" | "get";
/** Name of the email field your provider expects. */
field?: string;
placeholder?: string;
button?: string;
/** Small print under the form. */
note?: string;
as?: "h2" | "h3";
class?: string;
}
const {
title,
accent,
body,
action,
method = "post",
field = "email",
placeholder = "you@example.com",
button = "Subscribe",
note,
as: Heading = "h2",
class: className,
} = Astro.props;
const id = `vs-newsletter-${Math.random().toString(36).slice(2, 8)}`;
---
<section class:list={["vs-newsletter", className]}>
<div class="vs-newsletter__inner">
<div class="vs-newsletter__copy">
<Heading class="vs-display vs-newsletter__title">{title}{accent && <> <em>{accent}</em></>}</Heading>
{body && <p class="vs-newsletter__body">{body}</p>}
</div>
<form class="vs-newsletter__form" action={action} method={method}>
<label class="vs-sr-only" for={id}>Email address</label>
<div class="vs-newsletter__field">
<input id={id} type="email" name={field} placeholder={placeholder} autocomplete="email" required />
<button type="submit">{button}</button>
</div>
{note && <p class="vs-newsletter__note">{note}</p>}
</form>
</div>
</section>
<style>
.vs-newsletter {
container-type: inline-size;
background: var(--vs-page);
color: var(--vs-ink);
font-family: var(--vs-font-sans);
font-stretch: var(--vs-font-stretch);
}
.vs-newsletter__inner {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 28px;
max-width: var(--vs-container);
margin-inline: auto;
padding: var(--vs-section-y) var(--vs-gutter);
border-top: var(--vs-border-w) solid var(--vs-line-strong);
}
.vs-newsletter__title {
font-size: clamp(1.75rem, 5cqi, 3.25rem);
}
.vs-newsletter__body {
max-width: 28rem;
margin: 0.75rem 0 0;
font-size: 1rem;
line-height: 1.55;
color: var(--vs-ink-2);
}
.vs-newsletter__field {
display: flex;
gap: 6px;
padding: 6px;
border: var(--vs-border-w) solid var(--vs-line-strong);
border-radius: var(--vs-radius-pill);
background: var(--vs-surface);
box-shadow: var(--vs-pop-shadow, var(--vs-shadow-sm));
}
.vs-newsletter__field:focus-within {
box-shadow: var(--vs-focus-ring);
}
.vs-newsletter__field input {
flex: 1;
width: 0;
min-width: 0;
padding: 0 14px;
border: 0;
background: transparent;
color: var(--vs-ink);
font: inherit;
font-size: 0.9375rem;
}
.vs-newsletter__field input::placeholder {
color: var(--vs-ink-3);
}
.vs-newsletter__field input:focus {
outline: none;
}
.vs-newsletter__field button {
flex: none;
height: 40px;
padding: 0 18px;
border: 0;
border-radius: var(--vs-radius-pill);
background: var(--vs-ink);
color: var(--vs-ink-inverse);
font: inherit;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
}
.vs-newsletter__field button:hover {
opacity: 0.9;
}
.vs-newsletter__field button:focus-visible {
outline: 2px solid var(--vs-accent);
outline-offset: 2px;
}
.vs-newsletter__note {
margin: 12px 0 0;
padding-left: 14px;
font-size: 0.75rem;
color: var(--vs-ink-3);
}
@container (min-width: 820px) {
.vs-newsletter__inner {
grid-template-columns: minmax(0, 1fr) minmax(0, 26rem);
align-items: center;
gap: 64px;
}
}
</style> Original, MIT.