A ready-to-run fundraising campaign website: a fast, static, accessible page you set up by editing one file. Workflows deploy it to your own GitHub Pages or AWS account — no server to run, no card data to handle, and nothing hosted by anyone but you.
Authorship: The code, prompts, and documentation in this repo are co-written with Claude (Anthropic). I direct the work and review the output; Claude writes the code and prose. I'm an infrastructure operator, not a software engineer — please don't read this repo as a portfolio of coding ability.
This README has two readers. Find yours:
- For the client — this is your campaign, and this site will be yours.
- For the operator — you're standing the site up for someone else's campaign.
New here? Start with ADOPTION.md — it walks through
everything you need in one place: what you'll need before you start, the intake
questions, the deploy drill, and how to take the site down later if you ever
need to.
What you get: one page for your campaign, with four sections — a hero (your campaign name, tagline, goal, deadline, and an optional progress bar), your story, a donate button or widget hosted by your payment processor, and a contact / volunteer section. It's built as plain static HTML with system fonts, no trackers, and no JavaScript running in anyone's browser, so it loads fast and scores well on Lighthouse — Google's page-speed and accessibility checker. That one page is rung 1. The kit comes in rungs, steps you take one at a time, and rung 2 is a plain site for your whole organization; see Rung 2 below.
What you own — everything:
| Thing | Who owns it |
|---|---|
| This GitHub repository | You |
| The live website + domain | You |
| Your payment processor account + the money | You |
| Your AWS account (only if you choose the S3 option) | You |
The site lives in your account and runs from there. It doesn't depend on us to keep working, and we don't host it, hold your funds, or see your donor data.
A note on payments and safety. This kit never touches card data. The donate section either links to your processor's hosted donate page or embeds their widget, so the payment itself happens on the processor's own secure, PCI-compliant pages — PCI being the card industry's security standard. That's deliberate: your money and your compliance obligations stay with the processor, not with this website.
Everything about your campaign lives in campaign.config.ts:
name, tagline, goal, deadline, story, colors, donate widget, and contact info.
Open it, fill in your details, save. The full field reference is in
src/config.ts. On rung 2, the words on each page live in
content/ as well; the settings stay in this one file.
npm install # once
npm run dev # preview locally at the printed URL
npm run build # produce the static site in ./distSkip this if one campaign page is all you need. Rung 1 stays the default.
What you get: six pages instead of one — home, about, programs, events,
donate, and contact — with the same header menu and footer on every page. The
campaign page can stay as a seventh page at /campaign/. Each page is a
Markdown file in content/; Markdown is plain text where
a line starting with ## is a heading and - starts a list. The pages ship
with placeholder text that starts with "Replace this," so you can see what's
left. The donate page uses the same processor-hosted button or widget as
rung 1, and the contact page the same email button or form. There's still no
server, no admin screen, and nothing hosted by anyone but you.
How to switch: set rung: 2 in campaign.config.ts, fill in the
organization block next to it, and edit the files in content/. To preview
rung 2 before you commit to it, run SITE_RUNG=2 npm run dev. If the people who
edit your pages shouldn't have to learn git, docs/editing-without-git.md
sets up an optional browser editor, in your own accounts, that sends each edit
to you as a pull request.
What it costs: the same as rung 1. GitHub Pages is free, and the S3 path costs what AWS charges for a handful of small files. The pull-request check builds and checks both rungs, which uses a couple of minutes of GitHub Actions time per change: free on a public repository, and counted against the monthly free allowance on a private one.
How long: not timed yet. Rung 2 has its own drill in
DRY-RUN.md,
timed separately so the rung-1 number stays honest, and its first receipt will
go under receipts/. The setup steps are the same as
rung 1; the extra time is writing the pages, and that depends on how much you
already have written down.
Coming from another site? If your organization already has a WordPress or Squarespace site, the rescue import turns its export into these pages and prints what it couldn't carry over. The same runbook covers Wix, Weebly, Google Sites, and a site you've lost access to.
Pick any processor that can give you either an embed snippet (a block of
HTML you paste in) or a donate link. Set the matching field —
donate.embedHtml or donate.buttonUrl — in the config. A few common,
no-backend options, as examples only — choose whatever fits your campaign, we
don't endorse any one of them:
| Processor | How you wire it | Notes |
|---|---|---|
| Givebutter | Paste their widget/embed code into donate.embedHtml, or use your campaign URL as donate.buttonUrl. |
Popular for nonprofits/campaigns; hosted forms + embeds. |
| Zeffy | Paste the embed <iframe> into donate.embedHtml, or link the form URL. |
Advertises zero platform fees. |
| PayPal | Use a PayPal donate-button link or hosted button as donate.buttonUrl. |
Widely recognized; simple link-button. |
| Donorbox / Stripe Payment Link | Paste the embed into donate.embedHtml, or link the hosted page. |
Both provide hosted, embeddable donation pages. |
Whichever you pick, the payment itself happens on the processor's pages, not here. Confirm current fees, eligibility, and payout terms directly with them.
This kit doesn't ship a backend — there's no server of its own to receive a
form submission. In the config, set contact.email (renders a mailto:
button, the kind that opens the visitor's email app) and/or paste a hosted
form embed (Google Forms, Tally, Jotform) into contact.formEmbedHtml.
- GitHub Pages — free, the default. GitHub's own free static-site hosting.
In your repo, go to Settings → Pages → Source = GitHub Actions, then push
to
main.deploy-pages.ymlbuilds and publishes it for you. For a project site (you.github.io/repo/), setbase: "/repo/"in the config. - S3 + CloudFront — the paid variant. Amazon's storage and
content-delivery services, for a custom domain with a CDN — a network of
servers that serve your site from somewhere close to each visitor. See
deploy-s3.yml: it proves its identity to your AWS account with short-lived GitHub OIDC — a sign-in method that needs no stored password or key — so there are no long-lived AWS keys anywhere in this kit. It syncs to S3 and optionally clears the CloudFront cache. It ships switched off until you fill in four clearly-marked placeholders. This option costs money on AWS; Pages doesn't.
Two documents drive an engagement with a client:
INTAKE.md— the one-page questionnaire. Every answer maps to acampaign.config.tsfield or a setup step. Fill it in with the client first.DRY-RUN.md— the timed drill: from a filled intake to a live site on the client's own account, with a checklist and a table to record the elapsed time. The recorded time is the receipt. Run it once before a real engagement to surface the slow steps (DNS, the AWS OIDC role). Rung 2 has a second, separately timed drill in the same file.
The delivery rule (ADR-0007, fleet-wide): kits deploy into client-owned
repos/accounts — never Lentago-hosted. That's why both deploy workflows target
the client's own GitHub/AWS. See docs/adr/ for the decisions
behind the donate-embed and client-ownership design.
campaign.config.ts— the one file a client editscontent/— rung 2's pages, one Markdown file eachsrc/— the Astro site (layouts, section components, styles).github/workflows/deploy-pages.yml— free GitHub Pages deploy.github/workflows/deploy-s3.yml— opt-in S3 + CloudFront deploy (OIDC, no static keys).github/workflows/build-check.yml— PR gate: type check, build, WCAG 2 AA (pa11y) on every page, for both rungsscripts/rescue-import.mjs—npm run rescue-import: WordPress/Squarespace export to rung-2 pages (runbook)ci/a11y.mjs—npm run a11y: checks every page the last build produced, locally or liveINTAKE.md·DRY-RUN.md— intake questionnaire and timed runbookdocs/adr/— architecture decision records
🌱 Lentago Labs is a pro-bono operations practice for organizations that run on volunteers, donations, and one overworked tech person. Everything here is free to take, and we practice what we publish: our own estate runs this way, in the open. Start at the org profile.