From db071ba2be268f91421287afd11e8b91b760fd23 Mon Sep 17 00:00:00 2001 From: Chad Nuttall Date: Fri, 25 Sep 2026 09:55:39 -0400 Subject: [PATCH 1/9] Add /donate Support Saba page with typed recipient registry (#171) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visitors regularly ask how to support Saba beyond their trip; the site had no canonical answer. Adds a server-rendered /donate page with a typed DONATION_RECIPIENTS registry (empty pending owner-approved content — a graceful in-progress state renders instead of fabricated organizations), the repo-documented marine-park conservation contribution, and a trust note clarifying that donations go directly to each organization. Entry points stay out of the primary nav: footer Explore column, the /partners conservation section, and the /plan-your-trip island section. Sitemap, llms.txt, and every route inventory updated; new donation_click analytics event for outbound recipient CTAs. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- app/(en)/(content)/donate/page.tsx | 87 ++++++++++++ app/(en)/(content)/plan-your-trip/page.tsx | 6 + app/sitemap.ts | 1 + components/donations/donations-section.tsx | 112 ++++++++++++++++ components/footer.tsx | 1 + .../conservation-partners-section.tsx | 11 ++ data/donations.ts | 48 +++++++ docs/ANALYTICS_SEO.md | 5 +- docs/OPERATIONS.md | 1 + lib/analytics.ts | 1 + public/llms.txt | 1 + tests/e2e/accessibility.spec.ts | 1 + tests/e2e/link-integrity.spec.ts | 2 +- tests/e2e/locale-routing.spec.ts | 1 + tests/e2e/smoke.spec.ts | 2 +- tests/integration/donate.test.tsx | 125 ++++++++++++++++++ tests/integration/internal-links.test.tsx | 37 ++++++ tests/unit/seo.test.ts | 1 + 18 files changed, 439 insertions(+), 4 deletions(-) create mode 100644 app/(en)/(content)/donate/page.tsx create mode 100644 components/donations/donations-section.tsx create mode 100644 data/donations.ts create mode 100644 tests/integration/donate.test.tsx diff --git a/app/(en)/(content)/donate/page.tsx b/app/(en)/(content)/donate/page.tsx new file mode 100644 index 0000000..9df5d8e --- /dev/null +++ b/app/(en)/(content)/donate/page.tsx @@ -0,0 +1,87 @@ +import Link from "next/link"; +import { createMetadata } from "@/lib/metadata"; +import { PageHero } from "@/components/page-hero"; +import { DonationsSection } from "@/components/donations/donations-section"; +import { DONATION_RECIPIENTS } from "@/data/donations"; +import { OPERATIONS } from "@/data/operations"; +import { divingAnchors } from "@/lib/anchors"; + +export const metadata = createMetadata({ + title: "Support Saba", + description: + "Ways to support Saba beyond your visit — island conservation and community organizations you can donate to directly. Sea Saba never collects or processes donations.", + path: "/donate", +}); + +export default function DonatePage() { + return ( + <> + + +

+ Visitors fall for Saba quickly — the reefs, the trails, the quiet + villages, and the people who keep it all running. Many guests ask how + they can give something back once they're home, and this page is + our answer. +

+

+ The most meaningful support goes straight to the island organizations + doing the work — the people protecting the marine park, caring for the + community, and keeping Saba the place you remember. +

+ + {/* Your Visit Already Helps */} +
+

+ Your Visit Already Helps +

+

+ Everything we dive sits inside the Saba National Marine Park, and + every dive carries a conservation contribution of{" "} + ${OPERATIONS.conservationFees.marineParkPerDiveUsd + OPERATIONS.conservationFees.chamberContributionPerDiveUsd}{" "} + per diver — ${OPERATIONS.conservationFees.marineParkPerDiveUsd} to the + Marine Park and ${OPERATIONS.conservationFees.chamberContributionPerDiveUsd}{" "} + to the island's hyperbaric chamber fund. Snorkel trips contribute{" "} + ${OPERATIONS.conservationFees.snorkelParkPerPersonUsd} per person. + These fees are set by the park and the chamber, not Sea Saba, and they + fund the moorings, patrols, and emergency coverage that keep the + reefs healthy. +

+

+ + How the Saba Marine Park works + +

+
+ + {/* Ways to Give Back */} +
+

+ Ways to Give Back +

+

+ When you're ready to do more, a number of island organizations + welcome direct support. Every card below links out to the + organization's own website — your donation goes to them, not + through us. +

+ + + +

+ Any donation link on this page opens the organization's own + website in a new tab. Sea Saba never collects, processes, or retains + donated funds — each organization manages its own gifts and receipts. +

+
+ + ); +} diff --git a/app/(en)/(content)/plan-your-trip/page.tsx b/app/(en)/(content)/plan-your-trip/page.tsx index 6ac9f3e..8f931d7 100644 --- a/app/(en)/(content)/plan-your-trip/page.tsx +++ b/app/(en)/(content)/plan-your-trip/page.tsx @@ -939,6 +939,12 @@ export default function PlanYourTripPage() { +

+ Many visitors leave asking how they can give something back to the island.{" "} + + See ways to support Saba + . +

{/* FAQ */} diff --git a/app/sitemap.ts b/app/sitemap.ts index a34c262..2d404ce 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -28,6 +28,7 @@ const SITEMAP_ROUTES: { { path: "/about", changeFrequency: "monthly", priority: 0.7 }, { path: "/contact", changeFrequency: "monthly", priority: 0.6 }, { path: "/partners", changeFrequency: "monthly", priority: 0.6 }, + { path: "/donate", changeFrequency: "monthly", priority: 0.6 }, { path: "/terms", changeFrequency: "yearly", priority: 0.3 }, { path: "/privacy", changeFrequency: "yearly", priority: 0.3 }, { path: "/cookie-policy", changeFrequency: "yearly", priority: 0.3 }, diff --git a/components/donations/donations-section.tsx b/components/donations/donations-section.tsx new file mode 100644 index 0000000..712827c --- /dev/null +++ b/components/donations/donations-section.tsx @@ -0,0 +1,112 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { ExternalLink } from "lucide-react"; +import { TrackedOutboundLink } from "@/components/tracked-outbound-link"; +import type { DonationRecipient } from "@/data/donations"; + +const CATEGORY_LABELS: Record, string> = { + conservation: "Conservation", + community: "Community", + animals: "Animals", + youth: "Youth", + culture: "Culture", +}; + +interface DonationsSectionProps { + recipients: DonationRecipient[]; +} + +/** + * Recipient grid for /donate (#171). While DONATION_RECIPIENTS is empty the + * section renders a graceful in-progress note — no fabricated organizations + * may appear publicly. Cards always send visitors to the recipient's own + * site; Sea Saba never processes donations. + */ +export function DonationsSection({ recipients }: DonationsSectionProps) { + if (recipients.length === 0) { + return ( +
+

+ We're assembling a short list of Saba organizations we can + wholeheartedly recommend. Until it's ready, the most reliable + route is also the simplest:{" "} + + ask us + + {" "}— we live here, and we're happy to point you at a cause that fits. +

+
+ ); + } + + return ( +
+ {recipients.map((recipient) => ( + + ))} +
+ ); +} + +function DonationCard({ recipient }: { recipient: DonationRecipient }) { + return ( +
+ {recipient.image && ( + {recipient.imageAlt + )} +
+

{recipient.name}

+ {recipient.category && ( + + {CATEGORY_LABELS[recipient.category]} + + )} +
+

+ {recipient.description} +

+ {recipient.funds && ( +

+ Helps fund:{" "} + {recipient.funds} +

+ )} + +
+ {recipient.donationUrl && ( + + Donate directly + + )} + + {recipient.donationUrl ? "Visit website" : "Visit their website"} + +
+
+ ); +} diff --git a/components/footer.tsx b/components/footer.tsx index 50cef29..b79601e 100644 --- a/components/footer.tsx +++ b/components/footer.tsx @@ -25,6 +25,7 @@ const EXPLORE_LINKS = [ { label: "Courses", href: "/courses" }, { label: "About", href: "/about" }, { label: "Contact", href: "/contact" }, + { label: "Support Saba", href: "/donate" }, ] as const; const RESOURCE_LINKS: { label: string; href: string; external?: boolean; ariaLabel?: string }[] = [ diff --git a/components/partners/conservation-partners-section.tsx b/components/partners/conservation-partners-section.tsx index 2904fff..6382804 100644 --- a/components/partners/conservation-partners-section.tsx +++ b/components/partners/conservation-partners-section.tsx @@ -44,6 +44,17 @@ export function ConservationPartnersSection({ partners }: ConservationPartnersSe ))} + +

+ Want to support the island's conservation and community work?{" "} + + See ways to give back to Saba + + . +

); } diff --git a/data/donations.ts b/data/donations.ts new file mode 100644 index 0000000..05d5295 --- /dev/null +++ b/data/donations.ts @@ -0,0 +1,48 @@ +/** + * Registry of the Saba organizations and causes featured on /donate (#171). + * + * STATUS: intentionally empty pending owner-approved content. The public page + * renders a graceful "list in progress" state while this array is empty — do + * not add placeholder, sample, or unverified entries to make it look complete. + * + * To add a recipient, append one entry here — no page or layout changes are + * needed. Every entry must be owner-confirmed: name, description, and website + * come from the organization itself, and `donationUrl` (optional) must be the + * recipient's own donation page. Sea Saba never collects, processes, or + * retains donations, so there is no checkout or payment field here by design. + * + * Fields that must never be guessed: nonprofit/tax status, percentages or + * fee breakdowns, and any charitable claim. Leave `funds` unset unless the + * organization itself states what a gift supports. + */ +export type DonationCategory = + | "conservation" + | "community" + | "animals" + | "youth" + | "culture"; + +export interface DonationRecipient { + /** Organization or cause name, as the organization itself styles it. */ + name: string; + /** One or two sentences on who they are and what they do on Saba. */ + description: string; + /** Plain-language summary of what a donation helps fund — from the recipient's own materials only. */ + funds?: string; + /** The organization's own website — required; doubles as the fallback destination when no donationUrl exists. */ + website: string; + /** + * Direct donation page on the recipient's own site, when one exists. + * First-party recipient URLs only — never an intermediary and never a + * Sea Saba checkout (none exists; do not build one). + */ + donationUrl?: string; + /** Optional grouping label shown as a small pill on the card. */ + category?: DonationCategory; + /** Approved local asset under public/images — never a hotlinked third-party logo. */ + image?: string; + /** Alt text for `image`; required when `image` is set. */ + imageAlt?: string; +} + +export const DONATION_RECIPIENTS: DonationRecipient[] = []; diff --git a/docs/ANALYTICS_SEO.md b/docs/ANALYTICS_SEO.md index 46427b6..9aae3a0 100644 --- a/docs/ANALYTICS_SEO.md +++ b/docs/ANALYTICS_SEO.md @@ -36,6 +36,7 @@ | `phone_click` | Footer and contact page | Page parameters, link parameters, and legacy aliases | | `whatsapp_click` | Footer and contact form/page | Page parameters, sanitized link parameters, and legacy aliases | | `directions_click` | FindSeaSaba map card/tooltip | Page parameters, link parameters, and legacy aliases | +| `donation_click` | `/donate` recipient "Donate directly" CTAs — outbound links to each organization's own donation page | Page parameters, link parameters, and legacy aliases | | `ferry_link_click` | Plan-your-trip and local partner ferry links | Page parameters, link parameters, and legacy aliases | | `social_click` | Social, partner, accommodation, and outbound resource links | Page parameters, link parameters, `partner_name` where applicable, and legacy aliases | | `pdf_download` | Dive-log PDF export | Page parameters, `dive_count`, `unit_system` | @@ -69,8 +70,8 @@ diagnostic-only variables) and what each one does. `app/sitemap.ts` lists every canonical public route — `/`, `/diving`, `/dive-sites`, `/book`, `/plan-your-trip`, `/courses`, `/dive-log`, -`/visiting-yachts`, `/about`, `/contact`, `/partners`, `/terms`, `/privacy`, -`/cookie-policy` — driven by the `SITEMAP_ROUTES` table. It deliberately +`/visiting-yachts`, `/about`, `/contact`, `/partners`, `/donate`, `/terms`, +`/privacy`, `/cookie-policy` — driven by the `SITEMAP_ROUTES` table. It deliberately omits `lastModified`: no trustworthy content-modification date exists, and a build-time timestamp is a fake freshness signal crawlers discount. Add new public pages to the table; `tests/unit/seo.test.ts` guards the list. diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md index 5df7815..3effcfc 100644 --- a/docs/OPERATIONS.md +++ b/docs/OPERATIONS.md @@ -14,6 +14,7 @@ keeps its own wording and context. | `lib/contact.ts` | Contact-form field limits and the inquiry email builders (subject, structured body, `mailto:` URI). The form opens the visitor's own email app addressed to `CONTACT.email` — server-side sending was removed because a common `From` address collapsed all visitors into one Respond.io contact. Direct Respond.io ingestion is deferred pending a plan upgrade; see issue #104. WhatsApp stays a client-side handoff | | `lib/respond-io.ts` + `components/respond-io-widget.tsx` | Website Chat channel — the native Respond.io launcher (channel `451154`, public widget cId via `NEXT_PUBLIC_RESPOND_IO_CID`) injected after window load. Widget appearance, pre-chat fields, popup message and workflow routing are Respond.io dashboard settings, not website code. Per-visitor identity is handled inside the widget's own iframe. Launcher position: dashboard "Vertical/Horizontal Spacing" (single value for all viewports — effective offset is 25px + the setting). `app/globals.css` additionally translates the closed launcher `translate(36px, 36px)` — the ~57px visible bubble sits ~5px inside the 90x90 iframe's corner, so the vendor offset left ~53-54px of visible clearance vs Cookiebot's ~10-11px; the shift lands the bubble ~17-18px from the right/bottom edges. On the homepage the closed launcher (and greeting popup, same iframe) is hidden while the `[data-hero]` hero region is in view — `respond-io-widget.tsx` toggles `data-hero-in-view` on `` via IntersectionObserver and `app/globals.css` hides `iframe[state="widgetClose"]`; an open conversation (`widgetOpen`) is never hidden, and other routes show the launcher normally | | `lib/anchors.ts` | Route section ids shared by pages and `data/redirects.ts` | +| `data/donations.ts` | `DONATION_RECIPIENTS` — owner-approved organizations/causes shown on `/donate` (empty until recipients are approved; the page renders an in-progress state). Entries link out to each organization's own site — Sea Saba never processes donations | | `app/(en)/(content)/terms/page.tsx` | Cancellation, reschedule, refund, weather, and charter policy — the canonical legal terms | | Checkfront | Live availability, bookable inventory, transactional pricing. The site intentionally shows no prices; `/book` links into Checkfront | diff --git a/lib/analytics.ts b/lib/analytics.ts index ff58610..0eb9ca1 100644 --- a/lib/analytics.ts +++ b/lib/analytics.ts @@ -12,6 +12,7 @@ export type AnalyticsEvent = | "phone_click" | "whatsapp_click" | "directions_click" + | "donation_click" | "ferry_link_click" | "social_click" | "pdf_download" diff --git a/public/llms.txt b/public/llms.txt index b5d2689..050d1e3 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -19,6 +19,7 @@ informational pages are server-rendered HTML and readable without JavaScript. - [Contact](https://www.seasaba.com/contact): inquiries, bookings, WhatsApp, and location - [Book](https://www.seasaba.com/book): online reservation for dives, courses, and trips - [Recommended Partners](https://www.seasaba.com/partners): accommodations, restaurants, transportation, and local services +- [Support Saba](https://www.seasaba.com/donate): ways to support Saba organizations and causes beyond your visit — donations go directly to each organization - [Sea Saba Dive Log](https://www.seasaba.com/dive-log): recent dives and sightings from Sea Saba boats ## Policies diff --git a/tests/e2e/accessibility.spec.ts b/tests/e2e/accessibility.spec.ts index a8fc5e2..44e8436 100644 --- a/tests/e2e/accessibility.spec.ts +++ b/tests/e2e/accessibility.spec.ts @@ -30,6 +30,7 @@ const PUBLIC_PAGES = [ { path: "/dive-sites", name: "dive sites" }, { path: "/about", name: "about" }, { path: "/dive-log", name: "dive log" }, + { path: "/donate", name: "support saba" }, ]; const MOBILE_SCAN_PAGES = ["/", "/contact", "/book"]; diff --git a/tests/e2e/link-integrity.spec.ts b/tests/e2e/link-integrity.spec.ts index 74aaa8f..63df91d 100644 --- a/tests/e2e/link-integrity.spec.ts +++ b/tests/e2e/link-integrity.spec.ts @@ -8,7 +8,7 @@ import { test, expect } from "./fixtures"; const PUBLIC_PAGES = [ "/", "/diving", "/dive-sites", "/courses", "/plan-your-trip", "/visiting-yachts", "/about", "/contact", "/book", "/dive-log", - "/partners", "/terms", "/privacy", "/cookie-policy", + "/partners", "/donate", "/terms", "/privacy", "/cookie-policy", ]; // Static assets and framework URLs that legitimately appear as href/src values. diff --git a/tests/e2e/locale-routing.spec.ts b/tests/e2e/locale-routing.spec.ts index 2676c99..701eafa 100644 --- a/tests/e2e/locale-routing.spec.ts +++ b/tests/e2e/locale-routing.spec.ts @@ -11,6 +11,7 @@ const PUBLIC_ROUTES = [ "/book", "/dive-log", "/partners", + "/donate", "/terms", "/privacy", "/cookie-policy", diff --git a/tests/e2e/smoke.spec.ts b/tests/e2e/smoke.spec.ts index b52ff18..1a41915 100644 --- a/tests/e2e/smoke.spec.ts +++ b/tests/e2e/smoke.spec.ts @@ -1,7 +1,7 @@ import { test, expect } from "./fixtures"; import { legacyRedirects } from "../../data/redirects"; -const routes = ["/", "/diving", "/dive-sites", "/courses", "/plan-your-trip", "/about", "/contact", "/book", "/dive-log", "/partners", "/terms", "/privacy", "/cookie-policy"]; +const routes = ["/", "/diving", "/dive-sites", "/courses", "/plan-your-trip", "/about", "/contact", "/book", "/dive-log", "/partners", "/donate", "/terms", "/privacy", "/cookie-policy"]; for (const path of routes) { test(`@smoke public page ${path} renders without authentication`, async ({ page }) => { const response = await page.goto(path); diff --git a/tests/integration/donate.test.tsx b/tests/integration/donate.test.tsx new file mode 100644 index 0000000..d116d84 --- /dev/null +++ b/tests/integration/donate.test.tsx @@ -0,0 +1,125 @@ +import { describe, expect, it } from "vitest"; +import { render, screen, within } from "@testing-library/react"; +import DonatePage from "@/app/(en)/(content)/donate/page"; +import { DonationsSection } from "@/components/donations/donations-section"; +import { DONATION_RECIPIENTS, type DonationRecipient } from "@/data/donations"; +import { divingAnchors } from "@/lib/anchors"; + +// /donate (issue #171): the page explains how visitors can support Saba and +// routes donations straight to each organization's own website — Sea Saba +// never processes donations. Until the owner approves recipients, the shipped +// registry stays empty and the page must render a graceful in-progress state +// rather than fabricated organizations. + +const FIXTURE: DonationRecipient[] = [ + { + name: "Example Reef Fund", + description: "Protects the reefs around the island.", + funds: "Moorings, patrols, and reef monitoring", + website: "https://reef.example.org", + donationUrl: "https://reef.example.org/give", + category: "conservation", + }, + { + name: "Example Community Project", + description: "Community programs in the villages.", + website: "https://community.example.org", + }, +]; + +describe("donate page", () => { + it("renders a single h1, the intro, and the trust note", () => { + render(); + expect(screen.getAllByRole("heading", { level: 1 })).toHaveLength(1); + expect( + screen.getByRole("heading", { level: 1, name: /support saba/i }) + ).toBeTruthy(); + expect( + screen.getByText(/never collects, processes, or retains donated funds/i) + ).toBeTruthy(); + }); + + it("states the existing marine-park contribution and deep-links to it", () => { + render(); + const link = screen.getByRole("link", { + name: /how the saba marine park works/i, + }); + expect(link.getAttribute("href")).toBe(`/diving#${divingAnchors.marinePark}`); + }); + + it("renders a graceful in-progress state while the shipped registry is empty", () => { + render(); + expect( + screen.getByText(/assembling a short list of saba organizations/i) + ).toBeTruthy(); + // The empty state still offers a real next step. + expect(screen.getByRole("link", { name: /^ask us$/i }).getAttribute("href")).toBe( + "/contact" + ); + // No fabricated recipients: no card headings exist at all. + expect( + screen.queryAllByRole("heading", { level: 3 }) + ).toHaveLength(0); + }); + + it("keeps donation claims honest — no Sea Saba checkout language", () => { + const { container } = render(); + const text = container.textContent ?? ""; + expect(text).not.toMatch(/checkout|add to cart|processing fee/i); + // No tax-deductibility claim without owner-supplied documentation. + expect(text).not.toMatch(/tax[- ]deductible|501\(c\)/i); + }); +}); + +describe("donation recipient cards", () => { + it("renders name, category, description, and funding line per entry", () => { + render(); + expect( + screen.getByRole("heading", { name: "Example Reef Fund" }) + ).toBeTruthy(); + expect(screen.getByText("Conservation")).toBeTruthy(); + expect(screen.getByText(/protects the reefs/i)).toBeTruthy(); + expect(screen.getByText(/moorings, patrols/i)).toBeTruthy(); + expect( + screen.getByRole("heading", { name: "Example Community Project" }) + ).toBeTruthy(); + }); + + it("donation CTAs open the recipient's own site in a new tab with descriptive labels", () => { + render(); + const donate = screen.getByRole("link", { name: /donate directly to example reef fund/i }); + expect(donate.getAttribute("href")).toBe("https://reef.example.org/give"); + expect(donate.getAttribute("target")).toBe("_blank"); + expect(donate.getAttribute("rel")).toContain("noopener"); + expect(donate.getAttribute("rel")).toContain("noreferrer"); + expect(donate.getAttribute("aria-label")).toMatch(/opens in a new tab/i); + + const visit = screen.getByRole("link", { name: /visit example reef fund's website/i }); + expect(visit.getAttribute("href")).toBe("https://reef.example.org"); + }); + + it("falls back to a website-only CTA when no donationUrl exists", () => { + render(); + const card = screen + .getByRole("heading", { name: "Example Community Project" }) + .closest("article")!; + const scoped = within(card as HTMLElement); + expect(scoped.queryByRole("link", { name: /donate/i })).toBeNull(); + const visit = scoped.getByRole("link", { name: /visit.*website/i }); + expect(visit.getAttribute("href")).toBe("https://community.example.org"); + expect(visit.getAttribute("target")).toBe("_blank"); + }); + + it("shipped registry stays empty until owner-approved recipients land", () => { + // Issue #171 ships the architecture without recipients; if entries are + // added later they must satisfy the content contract instead. + expect(Array.isArray(DONATION_RECIPIENTS)).toBe(true); + for (const r of DONATION_RECIPIENTS) { + expect(r.name.trim()).not.toBe(""); + expect(r.description.trim()).not.toBe(""); + expect(r.website).toMatch(/^https:\/\//); + if (r.donationUrl) expect(r.donationUrl).toMatch(/^https:\/\//); + if (r.image) expect(r.imageAlt?.trim()).toBeTruthy(); + } + }); +}); diff --git a/tests/integration/internal-links.test.tsx b/tests/integration/internal-links.test.tsx index 7fb0406..0e9e8d6 100644 --- a/tests/integration/internal-links.test.tsx +++ b/tests/integration/internal-links.test.tsx @@ -8,6 +8,7 @@ import PlanYourTripPage from "@/app/(en)/(content)/plan-your-trip/page"; import VisitingYachtsPage from "@/app/(en)/(content)/visiting-yachts/page"; import TermsPage from "@/app/(en)/(content)/terms/page"; import PartnersPage from "@/app/(en)/(content)/partners/page"; +import DonatePage from "@/app/(en)/(content)/donate/page"; import { Footer } from "@/components/footer"; import { coursesAnchors, @@ -76,6 +77,7 @@ describe("canonical anchors resolve to real section ids", () => { // /diving anchors are already covered by operations-sourcing.test.tsx. const idCheckCases: [string, React.ReactElement][] = [ ["/", ], + ["/donate", ], ...cases.map(([path, element]) => [path, element] as [string, React.ReactElement]), ]; @@ -154,6 +156,41 @@ describe("specific-purpose links carry canonical fragments", () => { } }); + it("donate entry points resolve: footer, partners, and plan-your-trip", () => { + render(