Newsletter subscribe entry points + dedicated /subscribe signup - #1031
Conversation
Adds a "Get updates" nav item (mail icon) between "Get involved" and Donate, pointing at /join for now. Rendered leading-icon on desktop (WideNavbar) and trailing-icon on mobile (NarrowNavbar) so the label lines up with the other items. Also centers the nav row (align-items) so the taller item does not push the others up.
…ent signup The homepage newsletter box posted straight to Substack, which is a different thing from the CRM movement newsletter. Adds a joinHandoff mode to NewsletterSignup that instead sends the visitor to /join with the email prefilled (?subscribe-email=...), wiring OnboardingFlow to consume the previously-no-op initialEmail. Collage/learn keep the Substack behaviour.
✅ Deploy Preview for pauseai ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Now that the box leads to the signup form, "Subscribe to our newsletter" + "Subscribe" over-promised a one-click action. Heading -> "Get updates" (matches the nav), button -> "Sign up" (signals it continues to a form). Scoped to the homepage instance; Substack uses keep their copy.
Points "Get updates" (nav) and the homepage box at a new /subscribe post instead of /join. It reuses the same OnboardingFlow but with a newsletter title/subtitle and reads ?subscribe-email to prefill. Fixes the two /join-inheritance issues: wrong hero copy, and the nav highlighting "Get involved". Also gives "Get updates" its own active-state highlight. NewsletterSignup.joinHandoff -> handoffHref (destination configurable).
…k opt-ins Reworks /subscribe from reusing the full /join flow into a lightweight single-step signup, per Jonathan's design review: - SubscribeFlow: name/email/country/city, optional local-group and Substack opt-ins, privacy-policy microcopy (no consent checkbox — signing up is the consent) - global newsletter by default; chapter sharing only on the explicit local-group tick. Decouples chapter sharing from the privacy consent on the subscribe path; the /join form still bundles the two, untouched. - "do more" continues in place into the intent step instead of linking to /join, carrying the created record id so it updates rather than creating a duplicate - the continuation hides the email opt-in cards and second consent, so it can only add involvement, never re-ask or silently undo the subscribe-time choices. Chapter sharing is preserved on update, escalating only for Volunteer/Lead. - rename the nav entry "Get updates" -> "Subscribe"
- The continuation posted through /join's step-2 form (no subscribe_form marker), so its update hit the strict required-field validation and 400'd for any subscriber who omitted an optional field (name/city/country) — the common email-only case. Updates now skip the create-time required-field check, since they patch an existing record. - The continuation's step-2 Back button led to step 1 and its "just browse" path, which had no record_id and so created a duplicate record with chapter sharing forced true, overriding a declined opt-in. Hide Back on the continuation so that path is unreachable. - gdprConsent is no longer seeded true on the continuation (it was pre-checking visible consent boxes on reachable sub-forms); instead the continuation's step-2 submit is exempted from the consent gate, since consent was captured on the subscribe form. - SubscribeFlow's email prefill now applies once into an empty field, so a prefilled address (?subscribe-email) can be cleared to fix a typo. - Move the lookupChapter fetch into the stub branch, its only consumer; the live path never read the result.
The .field inputs set width:100% with their own padding, and the site's reset doesn't apply box-sizing to form controls, so on narrow screens the inputs rendered content-box and spilled past the card's right edge. Add box-sizing:border-box to the card and inputs, and tighten the card padding on mobile.
The "do more" continuation reused the /join flow but sat under the /subscribe page's "Subscribe" heading and newsletter intro, and its stepper showed a "✓ About you" step the subscriber never saw (the subscribe form). Both read as off. - Add a `showTitle` frontmatter flag (mirrors `showImage`) so a page can suppress the template h1 and own its heading. subscribe.md sets it and moves the intro into the component. - SubscribeFlow now renders a phase-aware heading: "Subscribe" (+ intro) for the signup, "Get involved" once they continue. No more newsletter framing over the volunteer form. - Drop the "About you" step from the stepper on the continuation, so it reads Intent -> (Volunteer form ->) Confirmed.
The global `form label { flex-direction: column }` rule made the .opt-in label
stack under the checkbox. Force flex-direction: row so the checkbox sits beside
its label.
- Match the /join form's wording ("your local PauseAI chapter") on the
local-updates opt-in, instead of the nav's "local group".
- Add the required-field star to the Email label, as /join step 1 does.
Per review, the subscribe form now requires all four fields (name, email, country, city) with the same required-field stars as /join step 1, rather than the lighter email-only form. Server validation and the submit gate updated to match; chapter sharing still keys off the explicit local-chapter tick.
…apping The bespoke desktop treatment (a bare LinkWithoutIcon with inline-flex, custom padding/negative margins and a larger font, plus align-items:center on .nav-links) disturbed the header's flex layout enough that the logo wrapped above the nav even with room to spare. Render it through the standard Navlink like every other item and drop the custom styles. The mail icon stays on the mobile panel; can revisit a desktop icon without the bespoke markup.
…t inline
The header was capped at --page-width (the 768px content column), so the sixth
top-level item ("Subscribe") pushed the nav links past the row and the logo
wrapped above them on every page, regardless of viewport width. Add a
--header-width (48rem) and apply it where the nav is width-capped: the hero
.menu-band nav, and the .wide-navbar inside the width-capped .layout grid on
other pages. The header now gets slightly more room than the content column, so
all six items sit inline beside the logo; genuinely narrow widths still center
the logo above the nav as before. Restores the desktop mail icon on Subscribe.
…n prefill - The homepage box's native form still posted straight to Substack, so a submit before hydration (or with JS off) bypassed the signup flow entirely. When a handoff target is set, the form's native action/method/field now mirror the hydrated goto(), landing on /subscribe with the email prefilled. - Apply the prefill latch to OnboardingFlow too: /join?subscribe-email=... could not be cleared to fix a typo, because the effect restored it as soon as the field went empty.
…ate hardening - The Subscribe nav item turned brand-orange on hover, which is invisible on the hero's orange band (the :hover rule outranked .inverted at equal specificity). Go black on hover when inverted, matching .navlink.inverted:hover. - Backing out of Volunteer/Lead in the "do more" step left "GDPR chapter share permission" true for someone who had declined chapter updates on the subscribe form. The continuation now reposts that signup-time choice, so a non-escalating intent restores it; /join's own volunteer updates are unaffected. - Updates only write name/country/city when non-empty, so a partial post can't blank fields the create collected. - Localize the homepage handoff target so a visitor on a localized homepage stays in their locale instead of landing on the English page.
The subscribe card was 560px against /join's 800px, so the card visibly jumped width when the "do more" step swapped one for the other in place. Match the width, and override the global 30rem form cap as OnboardingFlow does so the fields fill the card instead of leaving the extra width empty.
…that misstate the signup - The heading spacing was inverted relative to /join (19px under the title and 48px before the card, where /join has 48 and 19). Mirror the page template's hgroup rhythm the page suppresses, so it now measures identically. - The "do more" confirmation repeated "You're in, thanks for joining us" moments after the subscribe thanks screen, and its opt-in confirmations claimed "we'll connect you with your local PauseAI chapter" even for someone who declined the chapter box on the subscribe form. Both are hidden on the continuation, where those opt-ins were already made and confirmed.
On the continuation the person has already signed up, so that step records their intent and moves them on rather than submitting a signup. /join is unchanged.
The act-now path stopped repeating 'You're in, thanks for joining us' on the continuation, but the no-intent confirmation still greeted them with 'You're in.' moments after the subscribe thanks screen. Gate it the same way, and correct a comment that misdescribed why the inverted hover rule wins.
…age itself - Clicking "Subscribe" in the nav while already on /subscribe did nothing: the finished flow stayed on screen, so the link looked broken. It now returns to the start, cleared — which also keeps a second signup from being one click away from duplicating the last one. Only same-path navigations reset, so arriving from the homepage box still prefills the handed-off email. - Require an intent to continue on the "do more" step. It was clickable with nothing selected (the newsletter opt-in is seeded on, which satisfied the /join rule), submitting a no-op update and landing on a confirmation for a choice never made.
main added Turnstile bot protection to the shared submit action, which now 403s a submission without a valid token. The subscribe form posts to that same action, so it needed the widget too — without it the form would have been rejected outright wherever a site key is configured. Conflict resolution in the intent step keeps both sides: main's Turnstile widget and its canSubmit gate, and this branch's continuation rules (no second consent field, and an intent required to continue).
The reset that runs when Subscribe is clicked from the page itself had to re-list every field, so a new field would silently keep its value across a restart — and that reset is what stops a stale signup being resubmitted. One object with a blank factory means the two can't drift apart. Also drops an unused honeypot binding: the field posts natively and nothing reads it.
/join and /subscribe ask for chapter consent differently, and once a row is written nothing else says which form produced it — so the distinction has to be recorded at create time or not at all. Give /subscribe its own Signup source. Also stop writing Signup source on updates: it is provenance, and the volunteer step carries no subscribe marker, so it would have rewritten a /subscribe row as a /join one.
The Turnstile site key only allows the production hostname, so on a deploy preview the widget refuses to render (Cloudflare error 110200) and no token can ever exist. The submit button then stays disabled and the server 403s, which makes the signup form untestable on every preview — the one place reviewers click through it. Gate the check on whether the submission actually writes. In stub mode there is no record, no subscription and no mail to protect, so there is nothing for the check to defend; live deploys verify exactly as before. The client mirrors the same rule, and assumes live if it cannot reach the mode endpoint, so an unanswered request keeps the check required rather than dropping it. Deliberately not done: pointing previews at Cloudflare's test keys. The verifier refuses those outside dev on purpose, because a test secret accepts every token and would silently reopen the form to bots.
- Read isOnboardingLive() once and use it for both the check and the writes, so 'they cannot disagree' holds by construction rather than by relying on the platform keeping env vars stable within a request. - Only an explicit false relaxes the client-side check, so a malformed mode response can't drop it by being falsy. - join-form-flow.md said Turnstile runs regardless of mode, which is no longer true, and now records that ONBOARDING_LIVE=true has to be scoped to the Production context: set site-wide it makes previews live, which both brings the untestable-preview problem back and lets a preview run write real data.
Pulls in the fix so this branch's preview is clickable before #1046 lands — the Turnstile widget can't render on a preview hostname, which left the signup form untestable exactly where it gets reviewed. Once #1046 is in main the later merge is a no-op. Applies the same rule to SubscribeFlow, which gates its own submit on a token and so was blocked the same way. Conflict resolution keeps this branch's lookupChapter placement (stub-only) with the fix's single liveness read.
In stub mode the check is skipped, but the widget still mounted — and on a deploy preview it can't render, so reviewers saw 'The anti-spam check could not load' next to a working, enabled button. Render it only when a token is actually required.
…scribeFlow Conflict resolution keeps this branch's continuation rule (no second consent field) alongside the fix's gating of the widget itself.
Conflicts are this branch's copy of that fix meeting the trimmed version that went into main. Takes main's wording and its widget-waits-for-the-mode change, keeps this branch's continuation rule and its stub-only lookupChapter, and applies the same widget gating to SubscribeFlow.
…omepage box The homepage box carried the generic newsletter description, which described the Substack it no longer signs people up for; it now has its own text.
|
We now require name email country and city on Substack signup, so a bit confused why we need to change the airtable automations? Surely we capture everything needed for them? |
|
Unless we mean that if someone subscribes in a country without a national leader, then they get assigned to a global handler? |
|
@hturnbull93 Right now the Onboarder assignment + alert automation notifies an onboarder of someone's country about every new signup, and it doesn't know about the new "Also send me updates from my local chapter" checkbox. Same for Share US signups with PAI US. And picking Volunteer or Lead in the get-involved step only updates the row, so nothing fires and the onboarder never hears about it. I think I have the updates we should do to the automations nearly worked out. |
Ok cool, I think those templates got adjusted to include the action/intent they chose, right? |
hturnbull93
left a comment
There was a problem hiding this comment.
Lgtm, happy with the discussion and can see the new flow in the screenshots
Yeah, the intent is shown in the mail.
Good point, this means that the gap only really matters for volunteers.
Yes, the existing-leader check is in there: it looks up a chapter for their country and falls back to Irina + global onboarding when there isn't one. We can reuse this branch for people who leave the local-chapter box unchecked, so they get the global onboarder instead of a national one |
"We'll connect you with your local PauseAI chapter" promises contact that may not happen: whether a chapter reaches out to a newsletter signup is the open question in #1014. The label already says what the box does.
|
@RisingOrange Could you maybe update |
…le retry (#1049) * docs(join-form-flow): cover the /subscribe flow and its consent model The submit action names this document as the flow contract to keep in sync when its inputs or behaviour change, and #1031 changed both without touching it. Adds /subscribe as a third entry point, the create-versus-update axis the action now turns on, and a section on chapter sharing, which the two forms capture differently and which decides how the Airtable automations route a signup. Also corrects two statements that #1031 made false: that the basics are unconditionally required, and that OnboardingFlow has no initialEmail prop. * docs(join-form-flow): correct three claims and document continuation mode Review found the previous commit asserted things the code does not do. - Cut a sentence claiming the unconditional required-field check had broken the hand-off. Every create enforces all four fields, so no short row exists, and the hand-off reposts them anyway. The rule is robustness, not a fix. - Only OnboardingFlow logs the onboarding mode; SubscribeFlow fetches it to gate Turnstile and logs nothing. - The signup-time chapter choice is reposted by OnboardingFlow's step-2 form under isContinuation, not by every update, which the Signup source section already implied by noting the volunteer step carries no subscribe marker. Adds the constraint most likely to be broken by a future editor: Email, Intent, Email subscription and Data privacy policy agreed are written from the post on every call, so dropping the keep_informed hidden input would clear subscription flags silently. Also documents continuation mode, which changes the step machine the diagram describes, and drops the initialEmail note that now only narrates history. * docs(join-form-flow): tighten claims a second review found overstated - The subscription-preservation warning named one hidden input; there are two, one on the step-2 intent form and one on the step-3 volunteer form, and dropping either clears the flag. Naming one left the other looking safe to remove. - Data privacy policy agreed is hard-coded true on every call, not taken from the post like the fields beside it. - The submit action is not the only route to a Substack subscription: a NewsletterSignup without handoffHref, as the Collagen banner uses, posts straight to Substack. - The result shape splits on live versus stub, not create versus update. - Turnstile hostname checking is weaker than stated: an absent hostname is accepted by design, and it compares hostname rather than full origin. - Adds Paying Interest to the volunteer update fields, drops an implementation detail that carried no constraint, and stops calling the join flow four-step when a newsletter-only signup finishes at step 3. Also points at where the Airtable automation behaviour is documented, since nothing in this repository can establish it. * docs(join-form-flow): cut duplicated claims, and put the flagship hazard in the code A review of the document's direction, rather than of its accuracy, argued the unit to watch is claim surface: how many independently falsifiable statements need re-verifying whenever the action changes. This branch had been growing it. - Collects create-versus-update into one section. It was spread across the endpoint section, the Signup source paragraph and two validation bullets, so the next change to the action had three places to rot and a reviewer fixing one copy would miss the others. - Merges the two bot-protection sections, which had drifted into stating the Turnstile hostname check at two different strengths, and defers the mechanism to turnstile-verify.ts, which is commented in full. - Replaces the volunteer field inventory with a pointer to the block that writes it. That list is where a field went missing, and a second copy of a list is a sync liability rather than a tripwire. The create list stays: every field in it has a rule stated elsewhere, so it works as an index. - Drops the snippet inventory, the continuation-mode cosmetics, and the homepage plumbing narration, keeping the coupling that handoffHref carries. - Removes a pointer to a pauseai-civicrm file that is untracked and exists only on one machine, so no other reader could have followed it. Adds the comments the document cannot substitute for: the hazard about Email subscription being cleared is now stated at both hidden inputs in OnboardingFlow and at the field in the action, which is where someone deleting an apparently redundant input will actually be looking. * docs(onboarding): correct two stale comments in the submit action The chapter-routing comment said notifying the chapter stays a manual Airtable process. That has not been true for as long as the onboarder-alert automation has existed, and it is what led a reviewer to read a correct paragraph in join-form-flow.md as contradicting the code. It now says why the live branch has no use for the lookup: the automations run their own, and route on the chapter-share field this action writes. The header listed only the /join entry points, and claimed the document is the full flow contract while the body states most of the same rules inline. That instruction is what generates the duplication: told to keep a full contract in sync, the next person copies their rule into the document too. It now names both routes and says which half belongs where. * docs(onboarding): allow deliberate duplication between doc and comments The previous wording said a single-site rule belongs in a comment rather than copied into the document. The document then does exactly that for three rules the action already comments at their site, so the header contradicted the diff it shipped in, and the ban was too strong anyway: a rule important enough to document is usually worth stating at the line that governs it as well. The rule is now about where relying on one place fails, not about avoiding overlap. The document alone is what does not work, because whoever breaks the rule is editing the code and does not have it open. * docs(onboarding): restore a dropped contract and fix comments the merge left stale From a review of the commits nothing had looked at yet. - The /subscribe hidden-input trim dropped intent=Keep informed as noise. It is not: the route pins that value, so every row it creates starts there whatever the person picks later, and an Airtable automation keys on exactly that pair of signup source and intent. Restored. - The rewritten header named /join and /subscribe but not the embed route, which is itself an entry point and which the document lists as Route 2. A header that presents an inventory has to be complete. - Three comments in the action still described the pre-/subscribe topology: consent bundling as universal when it is a /join arrangement, and record creation and Substack subscription as step-2 events when /subscribe does both during its own signup. Those are exactly the site-local explanations the new convention says to trust, so leaving them stale undercuts it. - The Turnstile hostname check is also skipped in dev, and dev is independent of ONBOARDING_LIVE. - The widget remount is not universal: SubscribeFlow remounts on every result, OnboardingFlow skips its unexpected-error branch, so a retry after one of those reposts a spent token. * fix(onboarding): remount Turnstile after an unexpected submit error The submit handler reset the widget on success and on a validation failure but not on the unexpected-error branch. Verification spends the token whether or not the write then succeeded, so after a 500 the next attempt reposted a spent token and failed the spam check until the user reloaded the page. Hoisted the reset to run on every result, which is what SubscribeFlow already does. Found while checking a claim the flow document makes about this contract, which was true of one form and not the other.
Good point, updated it in #1049 |

Adds newsletter subscribe entry points and a dedicated
/subscribesignup, so people who just want updates can find and complete a signup without going through the full "Join" flow.Requested and discussed in this Discord thread.
Note
The Airtable side is done. The Members-table automations now read the local-chapter choice this form adds:
Signups through the existing Join form are unaffected: that form's privacy checkbox already covers chapter sharing, so every one of its rows carries the agreement. The broader intent/consent question in #1014 is untouched by this.
Screenshots
Entry points
A "Subscribe" item in the header
The existing signup box near the bottom of the homepage — it now carries the email into the form instead of subscribing straight to Substack
The signup
/subscribe
After signing up
"Do more" → Get involved
Continues in place, reusing the /join flow without creating a duplicate:
Choosing how to get involved
e.g. the volunteer path
What this changes
Nav: a "Subscribe" entry (mail icon) in the header, before Donate. Adding a sixth item pushed the logo onto its own line on every page, so the header now gets a little more width than the text column it used to share. That part is a site-wide change, worth a look on a few pages.
Homepage box: the existing newsletter box near the bottom of the homepage now leads into the signup form (carrying the email) instead of subscribing straight to Substack, and its heading reads "Subscribe".
/subscribe: a dedicated single-page newsletter signup — name, email, country, city, with two optional opt-ins:"Do more" continuation: after signing up, an optional step to get more involved (Act now / Volunteer / Lead). It continues on the same page rather than sending people to
/join, and updates the signup they just made instead of creating a second one.This step is the
/joinintent step, minus its two email checkboxes ("Keep me informed" and the Substack). On/jointhat is where those choices get made; here they were just made on the form a moment earlier, so repeating them would only invite someone to switch off the newsletter they came for. The choices they made are kept as they are.Consent behaviour
/joinform is unchanged (it still bundles the two).Notes for reviewers
ONBOARDING_LIVE; the Netlify preview runs in stub mode, so submitting the form writes nothing to Airtable or Substack — feel free to click through.Related: signup Step 2 proposal #1014. The CRM side of the local-chapter choice is done (PauseAI/pauseai-civicrm#493) — a decline recorded here now reaches CiviCRM, keyed on this form's own signup source.