尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 README ↗Next.js 14 site for RentBolt, deployed on Vercel. Listing data lives in monday.com; the site reads it, caches it in Redis, and renders it.
monday.com ──► lib/monday.js ──► Upstash Redis ──► pages
(5 boards) parse + shape 15 min cache
cron every 10 min
Nothing writes back to monday.com. Two things live outside monday:
| Data | Where | Why |
|---|---|---|
| Photo galleries | Redis rentbolt:gallery:<buildingId> | monday subitem boards don't reliably have the column we need (InvalidColumnIdException). Redis has no schema to fight. |
| Pricing | the inventory feed (see below) | monday subitem prices were missing on 63% of listings and go stale silently. |
| Saved-list leads | Redis rentbolt:lead:<id> + rentbolt:leads | Redis was already provisioned. Migrate to monday when the other lead forms are wired. |
Everything else — pricing, status, amenities, descriptions — is monday only. Edit there and it appears on the site within ~10 minutes.
monday.com generates a unique column id per board. lib/monday.js has one
COL map built from Montreal column ids and applies it to all five boards.
Any field in that map silently reads as empty on the other four — no error, no
warning, just missing data that surfaces weeks later as "the filter doesn't
work in Toronto".
This has caused at least four separate bugs: location columns, amenities /
inclusions / appliances, unitsAvail (which emptied the bed filters
everywhere but Montreal), and the promo column.
Before debugging anything that looks like missing data, run
/api/debug-columns. It lists, per board, every expected column that
doesn't exist there and suggests same-type replacements.
Two fields already work around this:
"2 Bed", "Studio")
as well as the dropdown, so they work on every board.| City | Board ID | Location column |
|---|---|---|
| Montreal | 3743206409 | location |
| Ottawa | 4955235841 | location |
| Toronto | 18402583974 | location_mm3yrv60 |
| London | 18401824343 | location_mm5eg13x |
| Kitchener-Waterloo | 5892819868 | location_mm3yr7wf |
| Mississauga | 18431630361 | location_mm3yrv60 |
| Ajax | 18430413833 | location_mm3yrv60 |
Only items with status Lease-Up or Stabilization are published.
Mississauga and Ajax were duplicated from the Toronto board, so they inherit
Toronto's column ids exactly — including the dropdowns in COL_EXTRA. That is
inheritance, not coincidence: a board rebuilt from scratch instead of duplicated
would get fresh ids and need its own entry.
Three Mississauga buildings sit on one street (1423 / 1447 / 1547 Mississauga
Valley Blvd), so all three publish as "Mississauga Valley Boulevard Apartments".
Known and accepted for now; the publicNameOverride hook below is the fix.
The board list is duplicated in /api/debug-boards and /api/debug-columns.
Adding a board means editing all three.
Building identity is stripped server-side in parseBuilding, before
anything reaches the browser. Masking at render time is not enough — the data
also sits in the JSON payload, the URL slug and the page metadata.
1445 Rue du Fort → Rue du Fort Apartments)lib/publicName.js honours a publicNameOverride field if a "Public Name"
column is ever added in monday.
| Variable | Used for |
|---|---|
MONDAY_API_KEY | reading the boards |
UPSTASH_REDIS_REST_URL / UPSTASH_REDIS_REST_TOKEN | cache, galleries, leads |
CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN | photo uploads |
CLOUDFLARE_ACCOUNT_HASH | building delivery URLs (has a fallback) |
ADMIN_PASSWORD | /admin/photos, /admin/leads and all /api/admin/* |
MONDAY_LEADS_BOARD_ID | optional — mirrors each lead onto a monday board. Absent = leads live in Redis only |
CRON_SECRET | authenticating the scheduled refresh |
CACHE_REFRESH_SECRET | manual cache clear |
NEXT_PUBLIC_CARTO_API_KEY | optional — switches the map to Carto Positron. Absent = keyless Esri Light Gray |
RENTBOLT_PRICING_URL / RENTBOLT_PRICING_API_KEY | the pricing feed. Absent = the site falls back to monday subitem prices. No NEXT_PUBLIC_ prefix — these must never reach a browser |
RENTBOLT_PRICING_REFRESH_MINUTES | optional — pricing refresh interval. Default 1440 (daily) |
Env vars load at build time. After changing one in Vercel you must redeploy, or the running deployment won't see it.
/admin/photos, signs in with ADMIN_PASSWORDFiles go browser → Cloudflare directly via Direct Creator Upload. They do not pass through Vercel, which caps request bodies at 4.5MB while these photos run 2–5MB. The API token stays server-side; only a one-time upload URL reaches the browser.
Known upload failures, both pre-checked with a clear message:
type.startsWith("image/")
check but Cloudflare rejects itCoverage: /api/photo-status.
Four forms capture leads. All four post to one of two endpoints, and everything
lands in one Redis sorted set (rentbolt:leads), newest-first:
| Form | Kind | Endpoint |
|---|---|---|
/find-a-place, and the modal on the home/search/detail pages | tenant | /api/leads |
/landlords partnership form | landlord | /api/leads |
| "Book a visit" on a building page | visit | /api/leads |
/saved — the saved-homes list | saved-list | /api/save-list |
Read them at /admin/leads (same ADMIN_PASSWORD as the photo tool),
filterable by kind and exportable to CSV.
Three deliberate decisions:
MONDAY_LEADS_BOARD_ID and each lead is also pushed to that
board — best-effort, and a failure is logged rather than shown to the visitor.lib/submitLead.js, which reads the response as text before parsing —
a 500 from Next returns an HTML page, and res.json() on that throws a
SyntaxError that would otherwise be shown to the visitor as the reason their
form failed. If Redis is unconfigured the visitor is told to email us instead.
Every form has a visible error state; none of them fake a thank-you.Operational
| Route | Purpose |
|---|---|
/api/cron-refresh | scheduled rebuild, every 10 min |
/api/cache-refresh | clear the cache manually |
/api/photo-status | photo coverage per city, and who still needs photos |
/api/pricing-status | whether the pricing feed is on, and price coverage per city |
Diagnostics — these exist because each one was written to chase a real bug.
| Route | Answers |
|---|---|
/api/data-health | start here. Everything missing across every board, ranked by cost. ?names=1 lists the buildings, ?city=X narrows |
/api/debug-columns | which expected columns don't exist on which board |
/api/debug-boards | per-board health; ?heavy=1 uses the production query shape. Also names active listings missing coordinates |
/api/debug-beds?city=X | parsed bedrooms per building, and how many have no unit data |
/api/debug-neighbourhoods?city=X | borough coverage and counts |
/api/find-building?name=X&board=ID | full column + subitem dump for one building |
Admin (all require the x-admin-password header)
/api/admin/ping · /api/admin/buildings · /api/admin/upload-url ·
/api/admin/save-gallery · /api/admin/leads (?kind=tenant, ?format=csv)
Public POST
/api/leads (tenant / landlord / visit) · /api/save-list (saved homes)
A city disappeared from the site
Almost certainly a monday query failure. /api/cron-refresh returns
boardFailures. The site backfills that city from a last-known-good snapshot
rather than dropping it, so this should degrade to slightly stale data rather
than a missing city.
Data looks stale
Two caches, and they are independent. Redis (10 min) and the page render.
/search is force-dynamic; the homepage and listing pages are ISR at 600s.
A page cache once served hour-old HTML while Redis was perfectly current —
check both before concluding anything.
A listing doesn't appear
In order: status must be Lease-Up or Stabilization; it needs coordinates
(/api/debug-boards names the ones missing them); then check
/api/debug-columns for that board.
Bed filters return nothing for a city
/api/debug-beds?city=X. noBedroomData counts buildings with no unit
subitems — those are invisible to every bed filter. That's a monday data gap,
not a code bug.
Cache keys
CACHE_KEY must be bumped when the building shape changes, in three files:
lib/monday.js, app/api/cron-refresh/route.js, app/api/cache-refresh/route.js.
Currently rentbolt:buildings:v21.
LAST_GOOD_KEY sits alongside it and must be bumped too — it was missed by
every bump up to v20 because the string doesn't contain buildings:vNN.
Each of these cost real debugging time. Don't undo them.
FormData/Blob. The form-data npm
package produces a malformed multipart body on Vercel's runtime.mondayQuery must be cache: "no-store". It once had
next: { revalidate: 3600 }, creating a hidden Next.js cache underneath
Redis. /api/cache-refresh only cleared Redis, so stale monday responses
were laundered as fresh indefinitely.catch { return [] } is how
Montreal vanished silently for weeks.0,0 mean "no location", not Null Island. Anything that
transforms coordinates must leave zeros alone, or a pin lands off Africa and
drags the map viewport across the Atlantic.Prices come from an inventory feed, not monday. lib/pricing.js fetches all
buildings in one authenticated server-side request, once a day, and keeps
the last good snapshot in Redis so it survives serverless invocations and any
feed outage.
Daily rather than the contract's 15 minutes, to keep load off the inventory
platform. The cost is latency: a price that changes just after a fetch is not
reflected until the next one, so a rented unit's price can show for up to 24
hours. Hit /api/cache-refresh to pull immediately after reconfirming
inventory. RENTBOLT_PRICING_REFRESH_MINUTES overrides the interval without a
deploy if that proves too slow.
Rules that are deliberate, not incidental:
The feed's freshness window is 7 days and is enforced on the feed side, not here. The site publishes whatever it receives.
name + geometry, round coordinates to 5 decimals, save to
data/boroughs-<city>.jsonSETS in lib/neighbourhoods.jsNo code changes. A city without a file simply doesn't show the filter.
Currently loaded: Montreal (38 boroughs) and Toronto (140 neighbourhoods). Toronto's file covers the City of Toronto only — buildings on that board located in Mississauga or elsewhere in the GTA resolve to no neighbourhood, which is the same behaviour as having no file at all.
Still to source: Ottawa, London, Kitchener-Waterloo.