This is a Next.js project bootstrapped with create-next-app. It is styled with Tailwind CSS and hosted on Vercel.
Use Node.js 22 or newer, then install the locked dependencies:
npm ci
npm run devCopy .env.example to .env.local and add a current Slack invite URL to test
the complete join flow. When NEXT_PUBLIC_RECAPTCHA_SITE_KEY is unset in
development, the /slack page skips Google reCAPTCHA but still validates the
location answer on the server.
The public URL to share is https://codeyoungstown.com/slack. The actual Slack
invite is stored only in SLACK_INVITE_URL; never add it to browser code or
commit it to the repository.
Production requires these Vercel environment variables:
SLACK_INVITE_URL: the currenthttps://join.slack.com/.../shared_invite/...URL.NEXT_PUBLIC_RECAPTCHA_SITE_KEY: the public reCAPTCHA Enterprise site key.GOOGLE_CLOUD_PROJECT_ID: the Google Cloud project containing the key.NEXT_PUBLIC_PROJECT_IDremains supported for the existing deployment.GOOGLE_SERVICE_KEY: the base64-encoded Google service-account JSON.RECAPTCHA_ALLOWED_HOSTNAMES: optional comma-separated hostname override; defaults tocodeyoungstown.com,www.codeyoungstown.com.RECAPTCHA_MIN_SCORE: optional score threshold; defaults to0.6.
The /api/slack-invite endpoint validates the honeypot, location answer,
reCAPTCHA token, token hostname, and risk score in one server-side request. It
returns the invite only as a non-cacheable redirect after those checks pass.
In Vercel Firewall, add a rate-limit rule for the request path
/api/slack-invite (a starting point is five requests per IP per hour), and
enable the Bot Protection managed ruleset in Challenge mode. Monitor denials
before tightening the limit so shared office and campus networks are not
blocked unnecessarily.
Slack invite links are revocable bearer credentials. Deactivate and replace a
link after exposure or abuse, update only SLACK_INVITE_URL, and continue
sharing the stable /slack URL.
Past Code Youngstown events are stored in data/events.json so the archive is
available at build time. The importer combines Meetup's public event feed with
the pre-Meetup records maintained in data/historical-events.json. Refresh the
generated archive with:
npm run scrape:eventsSite-managed events are maintained in data/upcoming-events.json. The homepage
and Events page merge those records with data/events.json, deduplicate by ID,
and classify them by date on each page request. The upcoming section is sorted
oldest first; the past section is newest first. An event becomes past at its
end time, or at its start time when no end time is available. Source filenames
and stale status labels do not control these sections. When no future or ongoing
events remain, the upcoming section displays "Nothing announced yet".
Finished site-managed records may stay in this file; no manual move or new
deployment is required for them to appear under Past Events.
The Events page links to /events.ics (calendar subscription) and /rss.xml
(RSS announcements). Both are served from the checked-in event data and update
when a new version of the site is deployed. Feed responses can be cached for
five minutes; subscribers refresh on their calendar or RSS app's schedule.
Both feeds include the complete archive and site-managed events, deduplicated
by id, with site-managed entries taking precedence. RSS sorts by announcement
time when known, otherwise by event time; an unknown publication date is omitted
rather than invented. Each feed item links to its event on the Events page.
When adding or editing an event:
- Keep
idunchanged when rescheduling or renaming it, including when moving it into the archive. IDs identify calendar entries and RSS items permanently. Before removing a finished announcement fromupcoming-events.json, move its complete record intodata/historical-events.jsonwith statusPAST, then runnpm run scrape:eventsto rebuild the archive. Keep its publication and revision metadata. Do not edit only the generateddata/events.json, since the next scrape overwrites it. Keep cancelled events in the same way. - Add
publishedAtas an ISO timestamp with an offset when first announcing it. Keep that timestamp when editing or archiving. The existing announcement uses its first repository publication commit timestamp. - Set
updatedAtto the edit's ISO timestamp and incrementsequence(starting at 1) when changing calendar details. Imported legacy calendar entries use the archive'sscrapedAttimestamp until explicit metadata is available. - Use
status: "CANCELLED"for cancellations and retain the record so calendar subscribers can receive the cancellation. - Supply
endTimewhen known for the website's date classification. Calendar feed entries always last two hours fromdateTime, including archived events. Event timestamps include UTC offsets and are exported as UTC, so calendar apps display them in the viewer's timezone. Calendar-wide changes carry a revision and modification timestamp so subscribers can recognize updated entries.
Subscribe by URL to receive future updates; importing a downloaded .ics file
creates a one-time copy. The Events page's calendar button opens a dialog with
a copyable subscription URL, Apple and Google Calendar instructions, and a
separate one-time download link. The subscription URL is
https://codeyoungstown.com/events.ics; downloads use /events.ics over HTTP(S).
Run npm test for feed formatting, timezone, identity, and cancellation checks
(Node 22.18+ or Node 24 is needed for the native TypeScript test loader).