Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions site/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Featherframe: a picture frame for bird lovers that shows who’s singing nearby</title>
<meta name="description" content="Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. A framed e-paper display, no subscription.">
<meta name="description" content="A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s. From $349, no subscription.">
<link rel="canonical" href="https://featherframe.app/">
<meta property="og:type" content="website">
<meta property="og:url" content="https://featherframe.app/">
<meta property="og:title" content="Featherframe: a picture frame for bird lovers that shows who’s singing nearby">
<meta property="og:site_name" content="Featherframe">
<meta property="og:description" content="Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. No subscription.">
<meta property="og:description" content="A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s. From $349, no subscription.">
<meta property="og:image" content="https://featherframe.app/img/og.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
Expand All @@ -21,7 +21,7 @@
<meta name="twitter:site" content="@wellsworkshop">
<meta name="twitter:creator" content="@wellsworkshop">
<meta name="twitter:title" content="Featherframe: a picture frame for bird lovers that shows who’s singing nearby">
<meta name="twitter:description" content="Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. No subscription.">
<meta name="twitter:description" content="A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s. From $349, no subscription.">
<meta name="twitter:image" content="https://featherframe.app/img/og.jpg">
<meta name="twitter:image:alt" content="A walnut Featherframe showing the Cedar Waxwing from The Birds of America, beside the Featherframe wordmark">
<meta name="theme-color" content="#FFFFFF">
Expand Down Expand Up @@ -52,7 +52,7 @@
"@type": "Product",
"@id": "https://featherframe.app/#product",
"name": "Featherframe",
"description": "Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. A framed e-paper display with no subscription.",
"description": "A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s. A framed e-paper display with no subscription.",
"image": ["https://featherframe.app/img/og.jpg"],
"brand": { "@id": "https://wells.ee/#org" },
"manufacturer": { "@id": "https://wells.ee/#org" },
Expand Down Expand Up @@ -84,8 +84,14 @@ <h1><span class="l1">Let the</span> <span class="l2">outside in.</span></h1>
</div>
</div>
<div class="copy">
<p>Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. A framed e-paper display with no subscription.</p>
<div class="cta" id="waitlist"><form class="wl" data-waitlist aria-label="Join the waitlist"><label class="sr" for="wl-hero-email">Email</label><div class="field"><input id="wl-hero-email" name="email" type="email" autocomplete="email" placeholder="you@example.com" required><button class="btn" type="submit">Join the waitlist</button></div><p class="wl-note">One email when Featherframe is available, and nothing else.</p><input class="trap" type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true"><p class="wl-status" role="status" tabindex="-1" hidden></p></form></div>
<p>A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s.</p>
<ul class="facts">
<li><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4.1 4.3 22.5 4.6 21.9 14.8 19.3 15 14.9 15.6 10.3 15.6 7.3 17.3 3.5 19 2.3 19.6 1.5 18 4.2 16.4Z"/><path class="solid" d="M12.4 12.4c-1.7-1-2.5-1.9-2.5-3 0-.8.6-1.4 1.3-1.4.5 0 1 .3 1.2.8.2-.5.7-.8 1.2-.8.7 0 1.3.6 1.3 1.4 0 1.1-.8 2-2.5 3z"/></svg><span>Hand-assembled in New England.</span></li>
<li><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 2.5h6a2 2 0 0 1 2 2v15a2 2 0 0 1-2 2H9a2 2 0 0 1-2-2v-15a2 2 0 0 1 2-2z"/><path d="M11 18.5h2"/></svg><span>Sets up from your phone. No extra hardware needed.</span></li>
<li><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><g transform="translate(0 4) scale(.8)"><path d="M3.5 18H12a7 7 0 0 0 7-7V8.5a3.5 3.5 0 0 0-6.4-2L2.5 19.5"/><path d="m19 8.5 2.2.5-2.2.5"/><path d="M6.8 18a5.5 5.5 0 0 0 3.6-9.5"/><path d="M9.5 18v2.5M13 17.4v3.1"/><circle class="solid" cx="15.6" cy="8.3" r=".55"/></g><path d="M20.5 9.5V3.2l2.5.8"/><circle class="solid" cx="19.4" cy="9.6" r="1.2"/></svg><span>Works almost anywhere: 7,000+ public bird stations worldwide.</span></li>
<li><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M3.5 12.2V4.5a1 1 0 0 1 1-1h7.7l8.3 8.3a1.4 1.4 0 0 1 0 2l-6.6 6.6a1.4 1.4 0 0 1-2 0z"/><circle cx="8" cy="8" r="1.5"/></svg><span>From $349. No subscription, no camera, no surprises.</span></li>
</ul>
<div class="cta" id="waitlist"><form class="wl" data-waitlist aria-label="Join the waitlist"><label class="sr" for="wl-hero-email">Email</label><div class="field"><input id="wl-hero-email" name="email" type="email" autocomplete="email" placeholder="you@example.com" required><button class="btn" type="submit">Join the waitlist</button></div><p class="wl-note">The first batch is just 50 frames. The waitlist hears first. No spam.</p><input class="trap" type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true"><p class="wl-status" role="status" tabindex="-1" hidden></p></form></div>
</div>
</section>

Expand Down
28 changes: 21 additions & 7 deletions site/public/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,13 @@ h2 { font-weight: 400; text-wrap: balance; }
.stage.live .poster { opacity: 0; transition: opacity .6s ease; }
.cover .cta .wl { position: relative; z-index: 2; }
.cover .copy > p { font-size: var(--t-lede); line-height: var(--lh-lede); margin: 0 0 26px; text-wrap: pretty; }
/* what a buyer asks first, under the sentence: who makes it, how it sets up, where it works, what it costs. Each with its icon, set
small and upright in the field's ink. */
.cover .facts { list-style: none; margin: -8px 0 24px; padding: 0; font-size: var(--t-small); line-height: var(--lh-lede); }
.cover .facts li { display: flex; align-items: flex-start; gap: 10px; text-wrap: pretty; }
.cover .facts li + li { margin-top: 6px; }
.cover .facts .icon { margin-top: 0; color: var(--ink); }
.cover .facts .icon .solid { fill: currentColor; } /* Connecticut's heart; the bird's eye and its note */

/* I. The art: a spread, the frame showing the flamingo in its left half,
pinned there, centred below the running head, while the text scrolls past. */
Expand Down Expand Up @@ -588,7 +595,7 @@ h2 { font-weight: 400; text-wrap: balance; }
--W: calc(100vw - 2 * var(--edge));
--V: calc(100svh - var(--nav));
/* under 1000px the sentence is set at the body size, so the frame has room beside it */
--gnd: 220px; --measure: 400px;
--gnd: 290px; --measure: 400px;
--F: clamp(72px, min((var(--W) - .9521 * var(--gnd)) / 5.0466, (var(--W) - var(--measure) - .9521 * var(--gnd)) / 1.2026,
(var(--V) - 112px - var(--gnd)) / 1.913), 240px);
--ht: calc(1.913 * var(--F) + var(--gnd)); /* the words: the headline's cap line to the note's last baseline */
Expand All @@ -614,10 +621,17 @@ h2 { font-weight: 400; text-wrap: balance; }
}
}
@media (max-width: 820px) and (min-aspect-ratio: 1/1) {
.cover { --gnd: 214px; }
.cover { --gnd: 286px; }
}
/* a small landscape window: the facts set tighter, so the words, the headline and the frame still share the screen */
@media (min-width: 701px) and (max-width: 999px) and (min-aspect-ratio: 1/1) {
.cover .facts { margin: -12px 0 18px; font-size: var(--t-fine); line-height: 1.3; }
.cover .facts li + li { margin-top: 2px; }
.cover .copy > p { margin-bottom: 20px; }
.cover .cta .wl-note { font-size: var(--t-fine); }
}
@media (min-width: 1000px) and (min-aspect-ratio: 1/1) {
.cover { --gnd: 248px; --measure: calc(17.7 * var(--t-lede)); }
.cover { --gnd: 341px; --measure: calc(17.7 * var(--t-lede)); }
.cover .copy > p { font-size: var(--t-lede); }
}
@media (min-width: 1100px) and (min-aspect-ratio: 1/1) {
Expand All @@ -627,7 +641,7 @@ h2 { font-weight: 400; text-wrap: balance; }
}
}
@media (min-width: 1200px) and (min-aspect-ratio: 1/1) {
.cover { --gnd: 211px; --measure: calc(22 * var(--t-lede)); }
.cover { --gnd: 305px; --measure: calc(21 * var(--t-lede)); }
}

/* A portrait window or a tablet: the phone's order, the headline
Expand All @@ -643,8 +657,8 @@ h2 { font-weight: 400; text-wrap: balance; }
--pad: 40px;
--g1: calc(.3 * var(--F)); /* the headline's last baseline to the frame's top edge */
--g2: 40px; /* the frame's foot to the sentence */
--words: 217px; /* the sentence, the waitlist's field and the note under it (200px under 821px) */
--fh: min(max(.4 * var(--V), var(--V) - 2 * var(--pad) - 1.513 * var(--F) - var(--g1) - var(--g2) - var(--words)), .72 * var(--W) / .843);
--words: 311px; /* the sentence, the facts, the waitlist's field and the note under it (299px under 821px) */
--fh: min(max(.35 * var(--V), var(--V) - 2 * var(--pad) - 1.513 * var(--F) - var(--g1) - var(--g2) - var(--words)), .72 * var(--W) / .843);
--hb: calc(var(--fh) / .7379);
--wb: calc(var(--hb) * .78376);
display: flex; flex-direction: column; justify-content: center; min-height: var(--V); padding: var(--pad) 0;
Expand All @@ -664,7 +678,7 @@ h2 { font-weight: 400; text-wrap: balance; }
}
}
@media (min-width: 701px) and (max-width: 820px) and (aspect-ratio < 1) {
.cover { --words: 200px; }
.cover { --words: 299px; }
}

@media (max-width: 820px) {
Expand Down
19 changes: 11 additions & 8 deletions site/test/site.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ test('social card and search basics', async ({ page, request }) => {
const product = ld['@graph'].find((n: { '@type': string }) => n['@type'] === 'Product');
expect(product).toMatchObject({ name: 'Featherframe', url: 'https://featherframe.app/' });
expect(product).not.toHaveProperty('offers');
await expect(page.locator('meta[name="twitter:description"]')).toHaveAttribute('content', 'Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. No subscription.');
await expect(page.locator('meta[name="twitter:description"]')).toHaveAttribute('content', 'A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s. From $349, no subscription.');
await expect(page.locator('link[rel="canonical"]')).toHaveAttribute('href', 'https://featherframe.app/');
expect((await request.get('/robots.txt')).ok()).toBe(true);
expect((await request.get('/sitemap.xml')).ok()).toBe(true);
Expand Down Expand Up @@ -68,11 +68,13 @@ test('every section and its key copy is there', async ({ page }) => {
await expect(page.locator('#specs .spec dd').nth(6)).toHaveText('Open source. See it on GitHub');
await expect(page.locator('#specs .spec dd').nth(6).getByRole('link', { name: 'See it on GitHub' })).toHaveAttribute('href', 'https://github.com/wr/featherframe');
await expect(page.locator('#specs .spec')).not.toContainText('Service');
// no price, sale or date anywhere (W-1020): the hero holds the signup alone, and each size is named for its display
// no sale or date anywhere (W-1020); the one price is the hero's "From $349", for buyers who ask what it costs first.
// The signup holds the form alone, and each size is named for its display
await expect(page.locator('.cover .cta > *')).toHaveCount(1);
await expect(page.locator('#specs .ho figcaption')).toHaveText(['10.3-inch · B&W', '13.3-inch · Color']);
await expect(page.locator('s')).toHaveCount(0);
await expect(page.locator('body')).not.toContainText(/Black Friday|November|December|holiday|\$(299|379|349|479)/i);
await expect(page.locator('body')).not.toContainText(/Black Friday|November|December|holiday|\$(299|379|479)/i);
await expect(page.locator('.cover .facts')).toContainText('$349');
await expect(page.locator('#specs .ho .diag span')).toHaveText(['10.3-inch display', '13.3-inch display']);
await expect(page.locator('#specs .dim')).toHaveCount(0);
// the close says the cover's line again
Expand All @@ -86,9 +88,10 @@ test('every section and its key copy is there', async ({ page }) => {
await expect(page.locator('.wall .folio')).toHaveText('From the collection');
await expect(page.locator('.close .words > p')).toHaveText('A framed e-paper display with no subscription.');
await expect(page.locator('body')).not.toContainText('Reserve');
await expect(page.locator('.cover .copy > p')).toHaveText('Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. A framed e-paper display with no subscription.');
await expect(page.locator('meta[name="description"]')).toHaveAttribute('content', 'Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. A framed e-paper display, no subscription.');
await expect(page.locator('meta[property="og:description"]')).toHaveAttribute('content', 'Your neighborhood birds, shown as they’re heard, in illustrations from the finest natural history books of the 1800s. No subscription.');
await expect(page.locator('.cover .copy > p')).toHaveText('A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s.');
await expect(page.locator('.cover .facts li')).toHaveText(['Hand-assembled in New England.', 'Sets up from your phone. No extra hardware needed.', 'Works almost anywhere: 7,000+ public bird stations worldwide.', 'From $349. No subscription, no camera, no surprises.']);
await expect(page.locator('meta[name="description"]')).toHaveAttribute('content', 'A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s. From $349, no subscription.');
await expect(page.locator('meta[property="og:description"]')).toHaveAttribute('content', 'A picture frame that shows the birds singing near your home, each in its hand-painted portrait from the 1800s. From $349, no subscription.');
// no exploded drawing anywhere: the reservation is its headline, the frame on its kickstand, its line and button
await expect(page.locator('.exploded, img[src*="exploded"]')).toHaveCount(0);
await expect(page.locator('.close > *')).toHaveCount(2);
Expand Down Expand Up @@ -185,7 +188,7 @@ test('every section and its key copy is there', async ({ page }) => {
await expect(page.locator(`${sel} .field .btn`)).toHaveText('Join the waitlist');
await expect(page.locator(`${sel} .field input`)).toHaveAttribute('placeholder', 'you@example.com');
// the note under the field says the one email, upright where the status that replaces it is italic (W-1021)
await expect(page.locator(`${sel} .wl-note`)).toHaveText('One email when Featherframe is available, and nothing else.');
await expect(page.locator(`${sel} .wl-note`)).toHaveText(sel === '.cover' ? 'The first batch is just 50 frames. The waitlist hears first. No spam.' : 'One email when Featherframe is available, and nothing else.');
await expect(page.locator(`${sel} .wl-note`)).toBeVisible();
await expect(page.locator(`${sel} .wl-note`)).toHaveCSS('font-style', 'normal');
const [field, note] = [await page.locator(`${sel} .field`).boundingBox(), await page.locator(`${sel} .wl-note`).boundingBox()];
Expand Down Expand Up @@ -373,7 +376,7 @@ test('the waitlist signs up, then asks which size', async ({ page }) => {
await shopRoute(page, 'waitlist', { ok: true, details: 'tok-1', state: 'interest', count: 3 }, signups);
await shopRoute(page, 'waitlist/details', { ok: true }, answers);
await page.goto('/');
await expect(page.locator('.cover .wl-note')).toHaveText('One email when Featherframe is available, and nothing else.');
await expect(page.locator('.cover .wl-note')).toHaveText('The first batch is just 50 frames. The waitlist hears first. No spam.');
await page.fill('#wl-hero-email', 'ada@example.com');
await page.click('.cover .wl button[type=submit]');
await expect.poll(() => signups.length).toBe(1);
Expand Down
Loading