Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 76 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Columbia Gadget Works website

Source for <https://columbiagadgetworks.org>. Built with [Hugo](https://gohugo.io) (no external theme; layouts live in `layouts/`), hosted on Cloudflare Pages.
Source for <https://columbiagadgetworks.org>. Built with [Hugo](https://gohugo.io) (no external theme; layouts live in `layouts/`), deployed as a Cloudflare Worker with static assets.

## Editing content

Expand All @@ -9,10 +9,12 @@ 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 is plenty); reference by filename |
| News posts | `content/news/<slug>.md` (copy an existing one; set `date`, `title`, optional `image`) |
| Tool pages | `content/tools/<tool>.md` (front matter holds `zone`, specs, training flag, wiki link) |
| Shop zones | `data/zones.yaml` (name, summary, and the accent colour each zone is themed with) |
| 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]` |

Expand All @@ -39,17 +41,83 @@ The site deploys as a Cloudflare Worker: Hugo's `public/` folder is served as st
- 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
- `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
- `SUBSCRIBERS_EXPORT_TOKEN`, required to export the mailing list (see below)
- `DISCORD_SIGNUP_WEBHOOK_URL` (optional), pings a channel on each list signup
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`.

## Shop zones

`/tools/` groups every tool page under the zone it belongs to. Zones are defined once in `data/zones.yaml`:

```yaml
- id: woodworking # must match the `zone:` value in a tool's front matter
name: Woodworking
accent: "#AB501B" # text and rules; keep it dark enough to read on white
summary: One sentence describing the zone.
```

Each tool page sets `zone: woodworking`. The accent is emitted into the stylesheet as a
CSS custom property (`.zone-woodworking { --zone-accent: ... }`), so adding a zone needs
no CSS edit. Zone order follows `zones.yaml`; tools within a zone follow their `weight`.

## 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`
so playback can start before the file finishes downloading.

## Class and event email list

`{{</* subscribe */>}}` renders the signup form. It is on `/classes/` and `/events/`, and
linked from the footer. Addresses are stored in Cloudflare KV, not a third-party mailing
service, so the list stays ours.

1. Create the namespace: `npx wrangler kv namespace create SUBSCRIBERS`
2. Paste the id it prints into the commented `kv_namespaces` block in `wrangler.jsonc`.
3. Add `SUBSCRIBERS_EXPORT_TOKEN` as a secret (`openssl rand -hex 32`).

Export the list:

```sh
curl "https://columbiagadgetworks.org/api/subscribers?token=$SUBSCRIBERS_EXPORT_TOKEN" -o subscribers.csv
```

Without that secret the export returns 404, so the list is never exposed by accident.
Records are keyed by lowercased email, so signing up twice updates one row. To remove
someone, delete the `sub:<their email>` key from the namespace.

`GET /api/health` reports which bindings the Worker can see (names only, never values).

## Spam protection (Turnstile)

The contact form and the signup form both use Turnstile when it is configured.
**Set both halves or neither:**

- `params.turnstileSiteKey` in `hugo.toml` (public, safe in git)
- `TURNSTILE_SECRET` as a Worker secret

The handlers verify only when the secret is present. If the secret is set but the site key
has not deployed, the forms render no widget, send no token, and every submission is
rejected. Deploy the site key first, then add the secret.

When creating the widget, list `columbiagadgetworks.org` (subdomains are covered
automatically) and the `workers.dev` hostname if you test there.

## After launch checklist

- Google Search Console: verify the domain, submit `/sitemap.xml`.
- Givebutter: fix the org profile URL to `https://`, create a **widget** for the donation campaign and paste its ID into `givebutterWidgetId`.
- Givebutter: fix the org profile URL to `https://`. (The donation widget is configured; `givebutterAccount` and `givebutterWidgetId` are set in `hugo.toml`.)
- Update `data/events.yaml` whenever a class is scheduled.

## License
Expand Down
95 changes: 77 additions & 18 deletions assets/css/site.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Columbia Gadget Works — site styles. Brand color comes from hugo.toml. */
/* Columbia Gadget Works site styles. Brand color comes from hugo.toml. */
:root{
--rust:{{ site.Params.brandColor }};
--rust-dark:#983C1E;
Expand All @@ -12,8 +12,10 @@
--line:#E5DED4;
--ok:#2F7A4B;
--warn:#9C6A00;
--font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
/* Ordinary, ubiquitous faces. Nothing is downloaded, so text paints on the
first frame and there is no layout shift when a webfont arrives. */
--font:Arial,"Helvetica Neue",Helvetica,sans-serif;
--mono:Consolas,"DejaVu Sans Mono","Courier New",monospace;
--wrap:1120px;
--r:10px;
}
Expand All @@ -23,8 +25,8 @@ body{margin:0;font-family:var(--font);font-size:17px;line-height:1.6;color:var(-
img{max-width:100%;height:auto;display:block}
a{color:var(--rust-dark);text-decoration-thickness:1px;text-underline-offset:2px}
a:hover{color:var(--rust)}
h1,h2,h3,h4{line-height:1.15;letter-spacing:-.01em;margin:0 0 .5em;font-weight:800}
h1{font-size:clamp(2rem,4.5vw,3.2rem)}
h1,h2,h3,h4{line-height:1.18;letter-spacing:-.005em;margin:0 0 .5em;font-weight:700}
h1{font-size:clamp(1.9rem,4.2vw,3rem)}
h2{font-size:clamp(1.5rem,2.6vw,2rem)}
h3{font-size:1.2rem}
p{margin:0 0 1em}
Expand All @@ -40,14 +42,15 @@ ul.plain{list-style:none;padding:0;margin:0}
ul.plain li{margin:0 0 .35em}

/* Buttons */
.btn{display:inline-block;padding:.6em 1.15em;border-radius:8px;font-weight:700;text-decoration:none;border:2px solid transparent;line-height:1.2;cursor:pointer;font-family:inherit;font-size:1rem}
.btn{display:inline-block;padding:.62em 1.2em;border-radius:3px;font-weight:700;text-decoration:none;border:2px solid transparent;line-height:1.2;cursor:pointer;font-family:inherit;font-size:1rem;transition:background .12s ease,border-color .12s ease,transform .06s ease}
.btn:active{transform:translateY(1px)}
.btn-primary{background:var(--rust);color:#fff;border-color:var(--rust)}
.btn-primary:hover{background:var(--rust-dark);border-color:var(--rust-dark);color:#fff}
.btn-light{background:#fff;color:var(--ink);border-color:#fff}
.btn-light:hover{background:var(--paper);color:var(--rust-dark)}
.btn-light{background:#fff;color:var(--ink);border-color:var(--ink)}
.btn-light:hover{background:var(--ink);color:#fff}
.btn-outline{background:transparent;color:var(--rust-dark);border-color:var(--rust)}
.btn-outline:hover{background:var(--rust-tint)}
.btn-lg{padding:.8em 1.4em;font-size:1.08rem}
.btn-lg{padding:.78em 1.5em;font-size:1.06rem}

/* Header */
.site-header{background:#fff;border-bottom:1px solid var(--line);position:sticky;top:0;z-index:50}
Expand Down Expand Up @@ -84,7 +87,7 @@ ul.plain li{margin:0 0 .35em}
.lead{font-size:1.2rem;max-width:62ch;color:inherit}
.hero .lead{color:#E7E3DC}
.hero-cta{display:flex;flex-wrap:wrap;gap:12px;margin:24px 0 20px}
.hack-chip{display:inline-block;background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.2);padding:.5em .9em;border-radius:999px;font-size:.95rem;margin:0}
.hack-chip{display:inline-block;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.18);border-left:3px solid var(--rust);padding:.5em .9em;border-radius:2px;font-size:.95rem;margin:0}

/* Cards / grids */
.three-up{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:20px}
Expand All @@ -97,7 +100,7 @@ ul.plain li{margin:0 0 .35em}
.band-dark .btn-light{color:var(--ink)}
.tool-ph{display:flex;align-items:center;justify-content:center;aspect-ratio:4/3;background:linear-gradient(135deg,#3a444f,#2B333C)}
.tool-grid .tool-ph img,.tool-card .tool-ph img{width:64px;height:64px;aspect-ratio:auto;opacity:.5}
.tool-card .tool-ph{aspect-ratio:16/10;background:var(--rust-tint)}
.tool-card .tool-ph{aspect-ratio:16/10;background:#F1ECE4}
.band-rust{background:var(--rust);color:#fff}
.band-rust a.btn-light{color:var(--rust-dark)}
.section-head{max-width:60ch;margin-bottom:28px}
Expand Down Expand Up @@ -136,6 +139,10 @@ ul.plain li{margin:0 0 .35em}
.page-hero{margin:0 0 32px}
.page-hero img{border-radius:var(--r);width:100%}
.prose{max-width:72ch}
/* Pages that lay out in columns opt out of the reading measure, so a two-column
block fills the 1120px wrap instead of being squeezed into 72ch. */
.prose-wide{max-width:none}
.prose-wide>.two-col>div>:first-child{margin-top:0}
.prose h2{margin-top:1.6em;padding-top:.2em}
.prose h3{margin-top:1.3em}
.prose ul,.prose ol{padding-left:1.3em}
Expand All @@ -154,17 +161,22 @@ ul.plain li{margin:0 0 .35em}
.callout.ok{border-left-color:var(--ok)}
.callout.warn{border-left-color:var(--warn)}
.callout p:last-child{margin-bottom:0}
.badges{display:flex;flex-wrap:wrap;gap:8px;margin:.4em 0}
.badge{font-size:.78rem;font-weight:700;padding:.25em .6em;border-radius:999px;background:#fff;border:1px solid var(--line);color:var(--muted)}
.badge-warn{border-color:#E6C98A;color:var(--warn);background:#FFF6E0}
.badge-ok{border-color:#B7DCC4;color:var(--ok);background:#E9F5EC}
/* Shop tags. Squared, stencilled, and labelled like the machines they describe:
pill-shaped pastel chips read as generic web UI, which is not what this place is. */
.badges{display:flex;flex-wrap:wrap;gap:8px;margin:.5em 0;align-items:stretch}
.badge{display:inline-block;font-family:var(--mono);font-size:.7rem;font-weight:700;text-transform:uppercase;letter-spacing:.07em;padding:.36em .65em;border-radius:2px;background:#fff;border:1px solid var(--line);color:var(--muted)}
.badge-ok{color:var(--ok)}
.badge-warn{color:#6B4A00}
.band-dark .badge,.hero .badge{background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.22);color:#EDE8E1}

/* Tools */
.tool-cards{list-style:none;padding:0;margin:24px 0 0;display:grid;gap:20px;grid-template-columns:repeat(auto-fill,minmax(320px,1fr))}
.tool-card{background:#fff;border:1px solid var(--line);border-radius:var(--r);overflow:hidden;display:flex;flex-direction:column}
.tool-card-img img{aspect-ratio:16/10;object-fit:cover;width:100%}
.tool-card-body{padding:18px 20px 20px}
.tool-card-body h2{font-size:1.25rem;margin-bottom:.1em}
.tool-card-body h3{font-size:1.2rem;margin-bottom:.1em}
.tool-card-body h3 a{text-decoration:none;color:var(--ink)}
.tool-card-body h3 a:hover{color:var(--rust-dark)}
.tool-card-body p{margin-bottom:.5em}
.tool-layout{display:grid;grid-template-columns:minmax(0,1fr) 340px;gap:40px;align-items:start}
@media (max-width:860px){.tool-layout{grid-template-columns:1fr}}
Expand All @@ -183,7 +195,7 @@ ul.plain li{margin:0 0 .35em}
.news-grid{list-style:none;padding:0;margin:24px 0;display:grid;gap:20px;grid-template-columns:repeat(auto-fill,minmax(300px,1fr))}
.news-card{background:#fff;border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
.news-thumb img{aspect-ratio:16/10;object-fit:cover;width:100%}
.thumb-blank{display:flex;align-items:center;justify-content:center;aspect-ratio:16/10;background:var(--rust-tint)}
.thumb-blank{display:flex;align-items:center;justify-content:center;aspect-ratio:16/10;background:#F1ECE4}
.news-body{padding:16px 18px 18px}
.news-body time{font-size:.85rem;color:var(--muted)}
.news-body h2{font-size:1.15rem;margin:.2em 0 .3em}
Expand Down Expand Up @@ -215,5 +227,52 @@ ul.plain li{margin:0 0 .35em}
.site-footer address{font-style:normal;line-height:1.7}
.hack-night{background:rgba(255,255,255,.06);border-radius:8px;padding:10px 12px;display:inline-block}
.footer-bottom{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px;border-top:1px solid rgba(255,255,255,.12);margin-top:36px;padding-top:16px;font-size:.85rem;color:#A9A39B}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
/* Zones. Colors come from data/zones.yaml so the palette lives in one place. */
{{ range hugo.Data.zones }}
.zone-{{ .id }}{--zone-accent:{{ .accent }}}
{{- end }}
.zone-nav{display:flex;flex-wrap:wrap;gap:8px;margin:28px 0 8px;padding:0;list-style:none}
.zone-chip{display:inline-block;font-size:.82rem;font-weight:700;text-decoration:none;color:var(--zone-accent);background:#fff;border:1px solid var(--line);border-radius:2px;padding:.42em .78em}
.zone-chip:hover,.zone-chip:focus-visible{border-color:var(--zone-accent)}
.zone{margin-top:44px;scroll-margin-top:84px}
.zone-head{border-left:4px solid var(--zone-accent);border-bottom:1px solid var(--line);padding:2px 0 12px 16px}
.zone-head h2{margin:0 0 .12em;font-size:1.35rem;color:var(--zone-accent)}
.zone-head p{margin:0;max-width:74ch;color:var(--muted)}
.zone .tool-cards{margin-top:18px}
.zone .tool-card{border-top:3px solid var(--zone-accent)}
.zone-more{margin:10px 0 0;font-size:.95rem}
.two-col.feature{align-items:center}
.two-col.feature h2{margin-top:0}
.eyebrow-sep{margin:0 .15em;opacity:.55}
.zone-link{color:var(--zone-accent,var(--rust-dark))}
.page.zone-woodworking,.page.zone-machining,.page.zone-digital-fabrication,.page.zone-electronics,.page.zone-crafting,.page.zone-metalworking{border-top:3px solid var(--zone-accent);margin-top:-1px}
/* Homepage tile accents */
.tool-grid li{--zone-accent:var(--rust)}
.tool-grid a{border-top:3px solid var(--zone-accent)}

/* Email signup */
.signup{border:1px solid var(--line);background:#fff;border-radius:var(--r);padding:24px;margin:2em 0}
.signup h2{font-size:1.3rem;margin:0 0 .2em}
.signup-lead{color:var(--muted);max-width:62ch}
.signup-form{margin:0}
.signup-fields{display:grid;grid-template-columns:1fr 1fr;gap:14px}
@media (max-width:600px){.signup-fields{grid-template-columns:1fr}}
.field.consent{margin:14px 0 16px}
.field.consent label{display:flex;gap:.6em;align-items:flex-start;font-weight:400;font-size:.95rem;margin:0}
.field.consent input[type=checkbox]{width:auto;flex:none;margin:.3em 0 0;accent-color:var(--rust)}
.signup-promise{margin-top:24px;border-top:1px solid var(--line);padding-top:16px}
.signup-promise h3{font-family:var(--mono);font-size:.72rem;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);margin:0 0 .6em;font-weight:700}
.signup-promise ul{padding-left:1.2em;margin:0 0 .7em}
.signup-promise li{margin:.32em 0;font-size:.95rem}
.signup-msg{margin-top:16px}
.signup-msg p:last-child{margin-bottom:0}

/* Video. Self-hosted, no third-party player, dimensions reserved to avoid layout shift. */
.video-figure{margin:1.6em 0}
.video-figure video{display:block;width:100%;height:auto;aspect-ratio:16/9;background:#14181c;border-radius:var(--r)}
.video-figure figcaption{margin-top:.55em;font-size:.9rem;color:var(--muted)}
.prose .video-figure{margin-left:0;margin-right:0}

@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}
.btn{transition:none}}
@media print{.site-header,.site-footer,.hero-media{display:none}}
Binary file added assets/img/battlebots-arena.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions content/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ In 2026 we began a deliberate transition from an informal "club" to a properly g

Columbia Gadget Works is a **membership organization**. Every member in good standing has one vote. Members elect four officers each April at the annual meeting, and the officers serve as the board of directors.

The shop itself is divided into **zones**: woodworking, metalworking, digital fabrication (laser, CNC, 3D printing), electronics, and casting. Each zone is run by a volunteer "zone lead" who sets it up, keeps it safe, and decides what training its tools require. Other volunteers take on functions like membership, classes, facilities, and communications.
The shop itself is divided into six **zones**: [woodworking, machining, digital fabrication, electronics, crafting, and metalworking](/tools/). Each zone is run by a volunteer "zone lead" who sets it up, keeps it safe, and decides what training its tools require. Other volunteers take on functions like membership, classes, facilities, and communications.

Decisions are written down on our member wiki. If it isn't on the wiki, it isn't policy.
Decisions are written down on our [member wiki](https://wiki.comogadget.casa/). If it isn't on the wiki, it isn't policy.

### Board of directors

Expand Down
Loading
Loading