Source for https://columbiagadgetworks.org. Built with Hugo (no external theme; layouts live in layouts/), deployed as a Cloudflare Worker with static assets.
Everything a volunteer normally touches is a Markdown or YAML file:
| What | Where |
|---|---|
| Pages (Visit, About, Donate, …) | content/<page>.md |
| News posts | content/news/<slug>.md (copy an existing one; set date, title, optional image) |
| Tool pages | content/tools/<tool>.md (front matter holds specs, training flag, wiki link) |
| Events calendar | data/events.yaml (recurring always shows; upcoming items disappear after their date) |
| Photos | assets/img/ (drop in a JPG/PNG, long edge 1600 px or less; reference by filename) |
| Video | static/video/ (MP4 muxed with -movflags +faststart; embed with the video shortcode) |
| Contact info, links, EIN, Givebutter IDs | hugo.toml under [params] |
| Menu | hugo.toml under [menus] |
Images referenced from front matter (image: foo.jpg) or with the shortcode {{</* img src="foo.jpg" alt="…" */>}} are automatically resized and served as WebP with srcset.
Edits can be made directly on GitHub (pencil icon → commit). Every push to main deploys; every pull request gets a preview URL.
hugo server # http://localhost:1313, live reload
hugo --minify # production build into public/Requires Hugo extended ≥ 0.146 (image processing). npm install fetches the pinned version into node_modules/.bin/hugo if you'd rather not install Hugo globally.
The site deploys as a Cloudflare Worker: Hugo's public/ folder is served as static assets and src/index.js handles the contact form at /api/contact. Config is in wrangler.jsonc.
- Workers & Pages → Create application → Import a repository → pick
ColumbiaGadgetWorks/website. - Build settings (Settings → Build after creation if the wizard skipped them):
- Build command: leave empty (or
npm run build);wrangler.jsoncruns the Hugo build itself - Deploy command:
npx wrangler deploy - Hugo comes from the
hugo-extendednpm package pinned inpackage.json, so noHUGO_VERSIONvariable is needed
- Build command: leave empty (or
- Secrets (Settings → Variables and Secrets, type Secret):
DISCORD_WEBHOOK_URL, webhook for the contact form channel (seesrc/contact.js)TURNSTILE_SECRET(optional), add only afterturnstileSiteKeyinhugo.tomlhas deployed
- Custom domain: Settings → Domains & Routes → add
columbiagadgetworks.organdwww(the zone must be in the same account). static/_redirectsmaps the old WordPress URLs;static/_headerssets caching and security headers. Both are honored by Workers static assets.
Manual deploy from a machine with Wrangler logged in: npm run deploy.
Drop an MP4 in static/video/ and a poster still in assets/img/, then:
{{</* video src="/video/thing.mp4" poster="thing.jpg" caption="What is happening." */>}}
Nothing loads until the visitor presses play (preload="none"), and the poster reserves the
layout box. Re-mux phone footage with ffmpeg -i in.mp4 -c copy -movflags +faststart out.mp4.
The "Get updates by email" box sits above the footer on every page
(layouts/_partials/updates-signup.html). Submissions go to src/subscribe.js and are stored
in a Cloudflare KV namespace bound as SUBSCRIBERS.
No setup needed. The binding in wrangler.jsonc deliberately has no id: Wrangler creates the
namespace on the first deploy and keeps it linked (automatic provisioning). This works in Workers
Builds because its default token has KV edit permission. It does not have D1 permission, so do
not switch this to a D1 database the same way.
To read the list: Cloudflare dashboard, Storage & Databases, KV, open the namespace. Each key
is sub:<email address>. Deleting a key unsubscribes that person. Signing up twice updates one
record rather than creating a duplicate, and each connection is limited to five signups an hour.
Optional: set SUBSCRIBERS_EXPORT_TOKEN as a secret to download the list as CSV from
/api/subscribers?token=.... Without it that address returns 404.
/donate/ embeds one Givebutter widget (givebutterWidgetId). Its form offers one-time, monthly
and yearly gifts; that choice, and the button's look, are both set in the Givebutter dashboard.
The contact form and the email signup both use Cloudflare Turnstile. The site key is set in
hugo.toml (turnstileSiteKey); it is public and safe in git. The secret lives only in the
Worker as TURNSTILE_SECRET (Workers & Pages, website, Settings, Variables and Secrets, type
Secret). Never commit it.
src/turnstile.js does the server-side check for both forms. It requires success, and also that
the token's action matches the form (contact or subscribe) and its hostname matches the site,
so a token minted for one form or another domain is refused.
Rollout order matters. With no secret set, the check is skipped. If the secret is added while the live pages have no widget, every submission is rejected for lacking a token. So the site key must be deployed first, then the secret added.
For local testing with wrangler dev, put Cloudflare's published always-pass test secret
(1x0000000000000000000000000000000AA) in .dev.vars (gitignored). Test tokens skip the action
and hostname checks, since Cloudflare answers them for a dummy host; the real secret never does.
- Google Search Console: verify the domain, submit
/sitemap.xml. - Givebutter: fix the org profile URL to
https://. (The donation widget is configured.) - Update
data/events.yamlwhenever a class is scheduled.
Site code: MIT. Content and photos: CC BY-SA 4.0 unless noted.