diff --git a/AGENTS.md b/AGENTS.md index ae70dae..4a482d6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,8 +16,8 @@ The source of the public site at `withmarfa.com`: Astro, built to static files a - `src/pages/`: one file per page. `404.astro` is what Cloudflare serves for a missing path. `sitemap.xml.ts` lists every page except those in its `UNLISTED` set. - `src/layouts/Base.astro`: the document head, the header, the column and the footer, used by every page. -- `src/components/`: the header (the Marfa mark, which leads home, a GitHub icon and the Docs pill), the footer (About, the Contact card and Privacy), the question that opens (one at a time within a group), and the sign-up form. -- `src/styles/global.css`: the color tokens, shared with the docs site's canvas and text colors. Light and dark follow the system; there is no toggle. +- `src/components/`: the header (the Marfa mark, which leads home, a GitHub icon and the Docs pill), which stays at the top as a page scrolls; the footer (About, Subscribe and Privacy); the Get Updates card with the sign-up form, opened from the footer and the home page; the Get in Touch card, opened from About; and the question that opens (one at a time within a group). +- `src/styles/global.css`: the color tokens, shared with the docs site's canvas and text colors, and the shared buttons and cards. Light and dark follow the system; there is no toggle. - `src/lib/analytics.ts`: PostHog, loaded after the page. - `public/`: copied as is. `_headers` sets the security headers and the Content Security Policy. `og.png` is the link preview image, rendered from `scripts/og-image.html`. - `worker/index.ts`: the Worker, which answers `POST /api/subscribe`. `worker/worker-configuration.d.ts` is generated by `pnpm types` from `wrangler.jsonc`; regenerate it whenever bindings change, or `pnpm check` fails. @@ -44,7 +44,7 @@ pnpm preview # build, then serve dist/ and the Worker with Wrangler as Cloudf ## Sign-ups -- The form on About posts to `/api/subscribe`. The Worker checks that the request comes from this site, limits each IP address to five tries a minute, ignores bots that fill the hidden `website` field, and keeps the address, lowercased, with the time in the `signups` table of the D1 database `withmarfa-website` (EU jurisdiction). A repeat address changes nothing and gets the same reply, so the form never reveals who is on the list. +- The form in the Get Updates card posts to `/api/subscribe`. The Worker checks that the request comes from this site, limits each IP address to five tries a minute, ignores bots that fill the hidden `website` field, and keeps the address, lowercased, with the time in the `signups` table of the D1 database `withmarfa-website` (EU jurisdiction). A repeat address changes nothing and gets the same reply, so the form never reveals who is on the list. - With JavaScript the form shows the result in place; without it the Worker redirects to `/subscribed` or `/subscribe-failed`. - Nothing logs an address. Invocation logs and traces are off, because they would keep each visitor's location and browser beside the time of their sign-up; only the Worker's own log lines are kept. To read the list, from a shell authenticated to the WithMarfa account: `pnpm exec wrangler d1 execute withmarfa-website --remote --command "SELECT email, created_at FROM signups ORDER BY created_at"`. To remove someone, `DELETE FROM signups WHERE email = '…'`. - A schema change is a new file in `migrations/`, applied with `pnpm exec wrangler d1 migrations apply withmarfa-website --remote` before the code that needs it is merged. The deploy does not apply migrations. diff --git a/src/components/Contact.astro b/src/components/Contact.astro new file mode 100644 index 0000000..835a945 --- /dev/null +++ b/src/components/Contact.astro @@ -0,0 +1,40 @@ +--- +/** + * The Get in Touch card. It shows the address before anything opens a mail + * app, and offers to copy it instead. + */ +const EMAIL = 'hello@withmarfa.com'; +--- + + + + + + diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 9bde17f..ed54d54 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -1,46 +1,20 @@ --- import { pagePath } from '../lib/path'; -/** - * The footer, and the Contact card it opens. The card shows the address before - * anything opens a mail app, and offers to copy it instead. - */ +import Updates from './Updates.astro'; + const path = pagePath(Astro.url); -const EMAIL = 'hello@withmarfa.com'; --- - - - + diff --git a/src/components/Header.astro b/src/components/Header.astro index 6309d8d..8971d8c 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -7,41 +7,68 @@ import { pagePath } from '../lib/path'; const path = pagePath(Astro.url); --- -
- - - - + +
+ + diff --git a/src/components/Subscribe.astro b/src/components/Subscribe.astro index c24ee8a..da7f041 100644 --- a/src/components/Subscribe.astro +++ b/src/components/Subscribe.astro @@ -1,33 +1,46 @@ --- /** * Signs someone up for updates. Without JavaScript the form posts and the - * Worker redirects to /subscribed or /subscribe-failed; with it, the result shows in place. The - * `website` field is left empty by people and filled by bots, which the Worker - * then ignores. + * Worker redirects to /subscribed or /subscribe-failed; with it, the result + * shows in place. The `website` field is left empty by people and filled by + * bots, which the Worker then ignores. */ +interface Props { + /** The card the form sits in, which its Close button hides. */ + card?: string; +} + +const { card } = Astro.props; ---
-
- - - - -
+ + +

Occasional updates, nothing else. Unsubscribe any time. Privacy

+
+ { + card && ( + + ) + } + +
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index f83d8fa..e3b7ed1 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -48,6 +48,7 @@ const image = new URL('/og.png', Astro.site).href;