Your own branded booking page, similar to Calendly, hosted on Netlify. Guests pick a time, the app checks every connected Google calendar for conflicts, and the booking goes on your calendar with a Zoom link. The guest also gets an invite.
public/ booking page (index.html), admin page, logo, background
netlify/functions/api.mjs API: availability, booking, Google sign-in, admin
lib/core.mjs time-zone + availability math (pure JS, unit-testable)
miccal.config.json your hours, meeting types, branding
netlify.toml routing + Node version
Your Google connections are stored in Netlify Blobs, Netlify’s built-in storage. There’s no database or disk to set up.
blurredspaces/cal.netlify.toml, so leave everything as it is and click Deploy.Under Project configuration → Environment variables, add:
| Key | Value |
|---|---|
ADMIN_PASSWORD |
a strong password for /admin |
GOOGLE_CLIENT_ID |
from step 2 |
GOOGLE_CLIENT_SECRET |
from step 2 |
BASE_URL |
https://book.blurredspaces.com. If you leave this out, it uses Netlify’s site URL. |
After adding or changing variables, redeploy with Deploys → Trigger deploy.
book.blurredspaces.com. Then, wherever your DNS is managed, add:
CNAME book → <your-site-name>.netlify.app
Your WordPress site at blurredspaces.com is not affected. Netlify issues HTTPS automatically.
calendar.readonly and calendar.events.https://book.blurredspaces.com/oauth/callback
https://<your-site-name>.netlify.app/oauth/callback
https://book.blurredspaces.com/admin, sign in, and click Connect Google account. Do this once for each Google account.
Until an account is connected, the booking page runs in demo mode: sample times, and no real events are created.
Meeting types with "location": "zoom" get a Zoom meeting created on your account at booking time. Until the Zoom keys below are set, they fall back to Google Meet.
tech@blurredspaces.com).meeting:write:meeting:admin. You can also add meeting:delete:meeting:admin, which lets the app clean up a Zoom meeting if a booking fails.From App Credentials, copy these values into Netlify’s environment variables, marking the secret as secret, then redeploy:
| Key | Value |
|---|---|
ZOOM_ACCOUNT_ID |
Account ID |
ZOOM_CLIENT_ID |
Client ID |
ZOOM_CLIENT_SECRET |
Client Secret |
ZOOM_USER_EMAIL |
(optional) which Zoom user hosts the meetings. The default is the account owner. |
/admin shows “Zoom connected” once it’s working. To go back to Meet for any meeting type, set its location to "google_meet".
/today)/today (same password as /admin) shows today’s and tomorrow’s events from every selected calendar, with Tomorrow collapsed until you tap it.
To add Slack unread alerts there, create a Slack app with a user token:
channels:read, groups:read, im:read, mpim:read,
channels:history, groups:history, im:history, mpim:history, users:readxoxp-).SLACK_USER_TOKEN (mark it secret). Optionally SLACK_TEAM_ID for deep links. Redeploy.What it shows:
users.counts was removed, and conversations.info returns unread fields for DMs only).Scanning is capped (50 DMs, 60 channels) and cached for 60 seconds to stay inside Slack’s rate limits.
/open, owner-only)A private page for putting anything on your own calendar, ignoring every rule the public page follows: no working hours, no minimum notice, no buffer, any length from 5 minutes to 12 hours, any day.
/admin, noindex, and not linked from the public site.miccal.config.json)Edit the file, commit, and push. Netlify redeploys automatically.
| key | meaning |
|---|---|
timezone |
Your time zone. Working hours are interpreted in this zone. Guests see times in their own zone. |
working_hours |
Open windows for each weekday, e.g. "mon": [["09:00","12:00"],["13:00","17:00"]] |
slot_interval |
Minutes between start times |
buffer_minutes |
Minimum gap kept free around existing meetings |
min_notice_hours / max_days_ahead |
How soon, and how far ahead, guests can book |
mirror_to_other_calendars |
Also put a copy of the event on your other connected account(s) |
event_types |
Your meeting types. Each slug becomes a link, e.g. /intro. location: "zoom", "google_meet", or plain text such as a phone number or address. |
This requires Node 22 or newer:
npm install
npx netlify-cli dev # http://localhost:8888, with .env for variables
For local Google testing, also add http://localhost:8888/oauth/callback as a redirect URI.
miccal) and are never sent to the browser.ADMIN_PASSWORD.