Skip to content

Repository files navigation

Columbia Gadget Works website

Source for https://columbiagadgetworks.org. Built with Hugo (no external theme; layouts live in layouts/), deployed as a Cloudflare Worker with static assets.

Editing content

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.

Running locally

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.

Cloudflare setup (Worker with static assets)

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.

  1. Workers & Pages → Create application → Import a repository → pick ColumbiaGadgetWorks/website.
  2. Build settings (Settings → Build after creation if the wizard skipped them):
    • Build command: leave empty (or npm run build); wrangler.jsonc runs the Hugo build itself
    • Deploy command: npx wrangler deploy
    • Hugo comes from the hugo-extended npm package pinned in package.json, so no HUGO_VERSION variable is needed
  3. Secrets (Settings → Variables and Secrets, type Secret):
    • DISCORD_WEBHOOK_URL, webhook for the contact form channel (see src/contact.js)
    • TURNSTILE_SECRET (optional), add only after turnstileSiteKey in hugo.toml has deployed
  4. Custom domain: Settings → Domains & Routes → add columbiagadgetworks.org and www (the zone must be in the same account).
  5. static/_redirects maps the old WordPress URLs; static/_headers sets caching and security headers. Both are honored by Workers static assets.

Manual deploy from a machine with Wrangler logged in: npm run deploy.

Video

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.

Email updates list

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 button

/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.

Spam protection (Turnstile)

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.

After launch checklist

  • 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.yaml whenever a class is scheduled.

License

Site code: MIT. Content and photos: CC BY-SA 4.0 unless noted.

About

Website source code

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages