Skip to content
amy-boPublic

About

AMYBO website

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

amybo.org

The website of AMYBO, a non-profit open source protein fermentation community: sustainable protein for all.

Built with Astro and Starlight, hosted on Cloudflare Pages. Event registration uses Events&I (beta), andeye's open source registration system on Pages Functions and D1; a copy lives in eventsandeye/.

Credits

  • Design and structure: Amy Andrews, whose 2025 Astro redesign (originally myndrws/amybo) is the base of this repository, with its full history.
  • Text: the site's own pages are written by Martin Currie with Claude; the pages marked source: hugo are carried over verbatim from the previous Hugo/Docsy site (amy-bo/pages, private until it is archived), whose contributors are credited in LICENSE-CONTENT.md.
  • Homepage photo: Ozark Drones on Unsplash, edited.

Every page's frontmatter records where its text came from (source: amy | hugo | new). REVIEW.md lists every page with its source and confirms each old Hugo URL still works.

Working on the site

Requires Node.js 22 or later (.nvmrc). A project-local Node 22 is also installed as a dev dependency, so npm run scripts use it even where the system Node is older.

npm install
npm run dev          # http://localhost:4321
npm run build        # static site into dist/ (needs PUBLIC_TURNSTILE_SITE_KEY)
npm run build:test   # same, with Cloudflare's Turnstile test key, for local testing
npm run check        # astro check + type-check of the functions and cron worker
npm run review       # regenerate REVIEW.md (after a build)
npm run test:a11y    # axe-core accessibility scan of dist/ (after a build)
npm run test:e2e     # full registration flow against a local D1 database (after build:test)
npm run test:forum   # the build-time forum feed (src/lib/forum.mjs): live path with a fixture, fallback paths
node scripts/og.mjs  # regenerate the Open Graph images after changing the logo or event title

Pages live in src/content/docs/; the file path is the URL. Images go next to the page that uses them, or in src/assets/photos/ (AMYBO's own photos, with their sources listed there). See the contribution guidelines.

src/content/docs/        pages: Amy's section pages, projects/ (electroPioreactor, CARMA Hub, PioFlo, other hardware,
                         past projects), background/, collaborate/, events/, privacy, 404
src/content/talks/       talks for event pages (one markdown file per talk)
src/components/          Amy's Hero and Header overrides, footer, click-to-play YouTube embed, talks list, forum feed
src/lib/forum.mjs        fetches forum.amybo.org/latest.json at build time; FORUM_FEED=off skips it, FORUM_FIXTURE=<file> stands in for the forum
src/assets/photos/       AMYBO photos from amy-bo/electroPioreactor (CC BY-SA 4.0), resized by Astro at build time
scripts/                 og.mjs (Open Graph images), review.mjs (REVIEW.md), a11y.mjs (axe-core scan)
public/                  robots.txt, og.png and og-event.jpg, _redirects
eventsandeye/            Events&I registration system (AGPL-3.0): server code, admin page, cron worker, schema, tests, Astro components
functions/               one-line Cloudflare Pages Functions wrappers that route to Events&I
seed/                    the 13 November 2026 event, its sessions and first joining instructions
workers/cron/            wrangler config for the Events&I cron worker
public/_redirects        every old Hugo /docs/ URL, redirected to its new home under /projects/, /background/ or /collaborate/
migration/hugo-map.json  every Hugo file and where it went

Events and registration

The 13 November 2026 get-together page is src/content/docs/events/2026-11-13-london.mdx.

Adding talks. Copy src/content/talks/_example.md to a new file, fill in the title, speaker, affiliation, bio and optionally a headshot (put the image in src/content/talks/headshots/), and set published: true. Until at least one talk is published, the page shows "Programme to be announced".

How registration works is described in the Events&I README: double opt-in, holds, waiting lists that only move when an admin clicks Promote, self-service changes, calendar invitations and host lists. For this event:

  • Places: 20 in person (remote unlimited), and 10 on each lab tour. The main registration deadline is 6 November, 12 noon; each tour can be booked or changed until it starts, while places remain. Change any of these on the admin page.
  • Sessions: 10:30 and 11:15 lab tours (optional, booked separately), 12:00 and 14:00 talks (in person and on Google Meet), 17:00 pub. Add the two Meet links, hosts and final times on the admin page under Sessions; people whose calendar entries change get an update automatically, and nobody else is emailed.
  • Calendars: in-person attendees get one entry for their day, remote attendees one per online talk session, each with reminders and add-to-calendar links.
  • Hosts: a session with a host email gets the attendee list whenever it changes (names, plus emails of people who opted in to share them).

The automatic emails are: confirm your email; a reminder with joining instructions for anyone who registers twice; joining instructions with calendar invitations on confirmation or promotion; calendar updates only when someone's entries change; cancellation confirmation; notifications to the organisers' mailbox (NOTIFY_EMAIL in wrangler.toml); host lists. Editing a page or saving new joining instructions never emails anyone.

Admin page: amybo.org/admin/rsvps, behind Cloudflare Access. It shows counts and every registration, and lets you:

  • change the in-person maximum, registration deadline and assumed travel time
  • edit sessions: times, location, Google Meet links, hosts, capacities and tour booking deadlines
  • promote people from waiting lists, or remove spam
  • download a CSV, or copy a BCC list for sending from Gmail (always paste into BCC, never To or CC)
  • save a new version of the joining instructions, which new registrants then receive
  • email all confirmed registrants, or a filtered group (in person or remote, a tour, people on an older joining-instructions version), now or at a scheduled time, and cancel a scheduled email
  • download the sent log as markdown: every joining-instructions version, who has which version, the sessions, and every message sent

Updating joining instructions without spamming anyone. Download the sent log and ask Claude to draft the next version plus a short "what's changed" message. Save the new version, then send the "what's changed" message to people on an older version, ticking counts as joining instructions version N. Everyone ends up with everything, and nobody gets the same information twice.

Data retention. A scheduled job deletes the event's registrations, their email records and host lists 30 days after the event ends. See the privacy notice.

Link pages (amy.bo/~name)

Link-in-bio pages for AMYBO volunteers and supporters, and AMYBO's own at amy.bo/links. Each page is a plain-text twisty list with a purely graphical map of the same links beside it (above it on phones). The map is laid out once on the server and drawn as static SVG; links on it work without any script, and only opening a group (or tapping the map on a phone) loads public/link-assets/graph.js, which grows the group's items out of it with CSS transitions. Hovering an item in either lights up its counterpart, in CSS alone. Volunteers get a diary with highlights; supporters a "How I support" note.

  • Code: src/links/ (model, renderer, icons, API, editor), routes in functions/[page]/ (/~name), functions/links/ (/links), functions/api/links/ and functions/admin/links/, the editor at src/pages/links/edit/. public/_routes.json limits Functions to the paths that need them; keep it in step with functions/.
  • Data: D1 tables lp_* and link_events (db/links/). Clicks and views store only the page, the link and the time.
  • People: by invitation from /admin/links/ (behind Cloudflare Access). They sign in with an emailed single-use link; no passwords.
  • Images: uploads go to the R2 bucket amybo-links (EU). The deploy binds it as LINKS_BUCKET only if it exists, so uploads stay off until it is created.
  • amy.bo: the amybo-redirects Worker in workers/amy-bo/ keeps the short links and serves /~name and /links from the Pages project under the amy.bo address. The deploy workflow deploys it once the repository variable DEPLOY_AMYBO_WORKER is true (the API token then also needs Workers Routes edit on the amy.bo zone). Until then, edit the Worker in the Cloudflare dashboard by pasting workers/amy-bo/index.js.
  • Tests: npm run build:test && npm run test:links (pages, clicks, invitations, sign-in, editing, diary, security checks and accessibility), and node --test workers/amy-bo/index.test.mjs.

Deploying from GitHub (the quick route)

.github/workflows/deploy.yml deploys everything from GitHub Actions on every push to launch-2026 or main, so nothing has to run on a laptop. It has two jobs:

  • build has no secrets at all: it installs, builds the site and hands it on as an artifact.
  • deploy runs in the production GitHub Environment and installs without running package scripts before it touches the Cloudflare token. It creates the D1 database in the EU jurisdiction if it is missing (amybo-rsvp-eu), applies the migrations and the insert-only seed, sets the secrets, deploys the site with its Functions and deploys the cron Worker.

Only the Pages project's production branch touches the live database and the cron Worker, and only it deploys the registration Functions. Other branches get a preview of the static pages alone, with no Functions and no database binding, so preview code can never reach live registrations. The first push to main makes main the production branch, so merging the launch PR hands production over automatically.

One-off set-up:

  1. Cloudflare API token (My Profile → API Tokens → Create Token → Custom): Account: Cloudflare Pages: Edit, Account: D1: Edit, Account: Workers Scripts: Edit, for this account only. Copy it and the Account ID.
  2. Turnstile (dashboard → Turnstile → Add widget): hostnames amybo.org, www.amybo.org and amybo-4p1.pages.dev (the project's own pages.dev address; amybo.pages.dev belongs to someone else), managed mode.
  3. Resend: add the domain amybo.org in the EU region, add the DNS records it shows (proxy off), verify, create an API key with sending access only.
  4. GitHub (repository → Settings → Secrets and variables → Actions). Secrets: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID, TOKEN_SECRET (openssl rand -hex 32; never paste it anywhere else), RESEND_API_KEY, TURNSTILE_SECRET_KEY. Variables: PUBLIC_TURNSTILE_SITE_KEY; SITE_URL (the address emails link to: https://amybo-4p1.pages.dev until amybo.org is attached, then https://amybo.org); ACCESS_TEAM_DOMAIN and ACCESS_AUD once step 6 below is done.
  5. Environment (Settings → Environments → production): add yourself as a required reviewer and restrict deployment branches to launch-2026 and main. Every deploy then waits for an approval from a two-factor-authenticated GitHub account.
  6. Cloudflare Access for the admin page, with two-factor sign-in: see step 6 of the manual set-up below. The admin page refuses any sign-in whose token does not report a second factor.

Two-factor authentication on the logins that reach other people's data

These accounts can reach every registrant's details, so each needs two-factor authentication:

Login Why Where to turn it on
Cloudflare (every member of the account) D1 database, Pages secrets, Access policy My Profile → Authentication → Two-factor; Manage Account → Members → require 2FA
GitHub (amy-bo organisation) pushes deploy to the live database Organisation settings → Authentication security → Require two-factor authentication
Resend delivery logs list every recipient Settings → Account → Two-factor authentication
Google account for hello@amybo.org receives organiser notifications and BCC exports Google Account → Security → 2-Step Verification
The admin page itself every registration Cloudflare Access policy with MFA and a session of 12 hours or less; enforced in code as well (second factor, ADMIN_EMAILS, ACCESS_MAX_AGE_HOURS)

Setting up Cloudflare (first deploy)

You need a Cloudflare account with the amybo.org zone (already there, since DNS is on Cloudflare), and admin rights on the amy-bo GitHub organisation.

  1. Create the database.

    npx wrangler login
    npx wrangler d1 create amybo-rsvp-eu --jurisdiction eu

    Put the printed database_id into both wrangler.toml and workers/cron/wrangler.toml, commit, then create the tables and the 13 November event:

    npx wrangler d1 migrations apply amybo-rsvp-eu --remote
    npx wrangler d1 execute amybo-rsvp-eu --remote --file seed/2026-11-13-london.sql
  2. Connect GitHub. In the Cloudflare dashboard: Workers & Pages → Create → Pages → Connect to Git. When GitHub asks, install the Cloudflare Pages app on the amy-bo organisation and give it access to only the website repository. Choose amy-bo/website, production branch main, framework preset Astro, build command npm run build, output directory dist. Add the build variable NODE_VERSION = 22. Pages reads the D1 binding and variables from wrangler.toml. On this route every preview would also get the Functions and the live database binding, so set Settings → Builds → Branch control → Preview branches: None, or use the GitHub Actions route above, which deploys previews as static pages only.

  3. Turnstile. Dashboard → Turnstile → Add widget for amybo.org (and amybo-4p1.pages.dev, the project's own pages.dev address), managed mode. Add the site key as the Pages build variable PUBLIC_TURNSTILE_SITE_KEY (the build fails without it, so a test key can never reach the live site), and the secret key as the secret TURNSTILE_SECRET_KEY.

  4. Resend. In Resend, add the domain amybo.org. Resend shows a few DNS records (an MX and a TXT record on a send subdomain, and a DKIM TXT record). Add them in Cloudflare DNS with the proxy off (DNS only), then click Verify in Resend. These records sit on subdomains, so the existing Google mail for the domain keeps working. Create an API key with sending access only.

  5. Secrets. In the Pages project → Settings → Variables and secrets, for Production (and Preview if you use previews):

    Secret Value
    TOKEN_SECRET output of openssl rand -hex 32; keep it stable, because changing it breaks every link already emailed
    RESEND_API_KEY the Resend key
    TURNSTILE_SECRET_KEY the Turnstile secret key

    Never set DEV_MODE on Cloudflare.

  6. Cloudflare Access for the admin page, with two-factor sign-in. Zero Trust → Access → Applications → Add → Self-hosted. Add these destinations: amybo.org/admin/*, amybo.org/api/admin/*, www.amybo.org/admin/*, www.amybo.org/api/admin/*, amybo-4p1.pages.dev/admin/*, amybo-4p1.pages.dev/api/admin/*, *.amybo-4p1.pages.dev/admin/* and *.amybo-4p1.pages.dev/api/admin/*. Add a policy Allow for the admins' email addresses, and turn on Require multi-factor authentication for the policy (Access's own MFA: an authenticator app or security key), or use an identity provider that enforces MFA. The email one-time PIN alone is not a second factor. With Cloudflare's per-application MFA (the application's Authentication → MFA → Customize MFA settings → Authenticator application; users enrol at <team>.cloudflareaccess.com/AddMfaDevice, which needs the App Launcher enabled for them), Access asks for the authenticator code but its tokens still report only onetimepin, so set ACCESS_MFA = "application" in wrangler.toml and keep that application setting on. Otherwise the admin page checks the token's methods itself and names them when it refuses; other method names can be accepted with ACCESS_MFA_METHODS. Set the application's session duration to 12 hours or less: the admin page refuses a sign-in token older than ACCESS_MAX_AGE_HOURS (default 12), and its refusal links to /cdn-cgi/access/logout to sign in again. The code also admits only the addresses in ADMIN_EMAILS in wrangler.toml (now hello@amybo.org), whatever the Access policy allows, so add any further admin there as well as in the policy. Save, then copy the application's Audience (AUD) tag into ACCESS_AUD in wrangler.toml, and your team domain (Zero Trust → Settings → Custom pages, e.g. amybo.cloudflareaccess.com) into ACCESS_TEAM_DOMAIN. Commit and push. The functions refuse admin requests without a valid Access token even if the Access policy is misconfigured.

  7. Cron Worker. Deploy the scheduled-jobs Worker and give it the same secrets:

    npx wrangler deploy --config workers/cron/wrangler.toml
    npx wrangler secret put TOKEN_SECRET --config workers/cron/wrangler.toml
    npx wrangler secret put RESEND_API_KEY --config workers/cron/wrangler.toml
  8. Test on the preview URL (https://amybo-4p1.pages.dev): register with your own email, confirm, change and cancel; open /admin/rsvps/ and check it asks you to log in.

Local development of the functions: copy .dev.vars.example to .dev.vars, fill in TOKEN_SECRET, then npm run build && npm run db:migrate:local && npm run pages:dev and open http://localhost:8788. Emails, including calendar attachments, are printed to the console and stored in a local outbox instead of being sent.

Cut-over from Netlify to Cloudflare Pages

  • Steps 1 to 8 above done, and the preview site checked page by page against the live site (REVIEW.md lists every page).
  • Event page facts confirmed: times, room and entrance, in-person maximum, tour capacities and registration deadline (set on the admin page).
  • Joining instructions version 1 checked on the admin page, including the Google Meet link and room details (or a note that they will follow).
  • Resend domain shows Verified; a test registration email arrives and is not in spam.
  • Gate, not a tick-box: calendar invitations checked with real sends to a Gmail and an Outlook address. The invitation shows as an invitation (check the raw message has Content-Type: text/calendar; method=REQUEST), adds to the calendar, and a session time change produces an update rather than a duplicate. If Outlook only shows a file, tell andeye before opening registration.
  • Session hosts, the two Google Meet links and final times entered on the admin page.
  • In Pages → Custom domains, add amybo.org and www.amybo.org. Cloudflare replaces the existing DNS records that point at Netlify. Wait for the certificate to show Active.
  • Check https://amybo.org, a few old URLs (for example /docs/overview/, /docs/pioflo/pioflo-v0.01/, /about/), the event page, registration end to end, and that /admin/rsvps/ needs a login.
  • In Netlify, remove the custom domain from the old site, then delete or disable the site so it stops building.
  • Archive the amy-bo/pages repository with a README note pointing to amy-bo/website.
  • Update links that point at the old repository (forum, YouTube descriptions, GitHub organisation profile).

Rollback: in Cloudflare DNS, point amybo.org back at the Netlify site (the Netlify site keeps working until it is deleted), so do not delete it until the new site has run cleanly for a while.

Changes

Done work is logged in CHANGELOG.md; what remains is in TODO.md.

Licence

Website code MIT (LICENSE); Events&I in eventsandeye/ AGPL-3.0; page text CC BY 4.0; AMYBO photos and images CC BY-SA 4.0. Details in LICENSE-CONTENT.md.

About

AMYBO website

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages