The campaign to make New York the first city in the Americas to endorse the UN Open Source Principles.
Live: https://unnyc.wegov.nyc
Built by WeGov.NYC and Sarapis. Not affiliated with the United Nations or any government agency.
npm install
npm run dev # http://localhost:3000No .env is required for local development — src/lib/api.js defaults to the
live CMS. Copy .env.example to .env.local if you need to point elsewhere.
- Next.js 16 (App Router, Turbopack), React 19
- Leaflet for the world map of governments advancing open source
- CSS cascade layers, no CSS framework, no build step beyond Next
- Deployed on Vercel
A hub plus five reader paths. Each page leads into the next, so it reads as a
funnel or as a pick-your-entry-point:
/ → /start → /principles → /crosswalk → /success → /resources.
| Route | Purpose |
|---|---|
/ |
Hub — what UNNYC is, then four cards routing to the paths below |
/start |
"I'm new to government open source" — vocabulary, the global map, the UN's timeline |
/principles |
The UN Open Source Principles + what each means for NYC |
/principles/document |
The principles, standalone + printable |
/crosswalk |
"Why it matters to NYC" — six reasons the city should adopt open source |
/success |
"What success looks like" |
/campaign |
Chooser: sign the public letter, or formally endorse |
/campaign/sign |
The open letter + signature form + published endorser wall |
/campaign/endorse |
Formal organizational endorsement form |
/campaign/endorse/document |
Printable declaration for City officials |
/resources |
Reference directory |
/resources/guide |
Long-form briefing: the UN system & NYC government technology |
/contact |
Contact form (name, email, message) — linked from the footer |
Every page's copy is one markdown file in content/. Frontmatter
holds the structure (titles, card lists, the eight principles), the body holds the
prose. The site renders these at build time, so the file is the source of truth —
no CMS, no sync step.
| Route | File |
|---|---|
/ |
content/home.md |
/start |
content/start.md |
/principles |
content/principles.md |
/principles/document |
content/principles.md (shared with /principles) |
/crosswalk |
content/crosswalk.md |
/success |
content/success.md |
/campaign |
content/campaign.md |
/campaign/sign |
content/sign.md |
/campaign/endorse |
content/endorse.md |
/resources |
content/resources.md |
/resources/guide |
content/guide.md |
/contact |
content/contact.md |
Full conventions, markers and gotchas: docs/EDITING-CONTENT.md. What's still in components (nav labels, footer, form messages): docs/CONTENT-MAP.md.
npm run lint:content guards these files and runs both as prebuild and as
the Validate content GitHub Action on every push and PR. It catches invalid
YAML, an odd number of " on a frontmatter line, a frontmatter slug with no
matching ## slug section, and duplicate ### Label keys. It exists because an
unterminated quote in home.md broke a production build, reporting the error
four lines below the actual mistake — and because the same class of typo can
parse cleanly and silently eat a key instead. Edit content through a pull
request: pushing main deploys, and the PR run is the check that catches this
before it ships.
src/lib/content.js does the parsing (gray-matter + marked) and applies three
conventions so plain markdown keeps the design: [text](gloss:ospo) renders a
glossary term with its definition as a hover tooltip, external links open in a new
tab while internal ones don't, and a blockquote whose last line starts with an
em-dash becomes a pull-quote with a <cite>.
| Flow | Destination | Notes |
|---|---|---|
Individual signature (/campaign/sign) |
Payload CMS → campaign-endorsements |
Arrives unpublished. Publishing an entry in the Payload admin is the review step that puts a name on the public endorser wall. Email is never exposed publicly. |
| "Get updates" email | Payload CMS → campaign-signups |
Best-effort; never blocks a signature. |
Formal org endorsement (/campaign/endorse) |
Payload CMS → campaign-endorsements (kind: organization) |
Same collection and review step as an individual signature. Approved entries appear on the public endorser wall. |
Contact message (/contact) |
Payload CMS → contact-submissions |
Added 2026-08-07. Needed no CMS change — the collection already existed for sarapis.org with exactly these fields. Its website field is a honeypot: Payload rejects any submission that fills it, so .unnyc-cmp-form__hp must stay display:none. Not brand-scoped, so ContactForm.js appends a "Sent from unnyc.wegov.nyc" line — the only thing distinguishing these from sarapis.org's. Nothing emails you; messages sit in the admin. |
Both endorsement flows now land in the same Payload collection, separated by
kind. They used to be split — org endorsements went to a Google Sheet via an
Apps Script webhook — which meant a second place to look, a bearer-capability URL
held as a Vercel secret, and a destination Payload had already modelled
(kind: organization, website, contactName) and the endorser wall already
rendered. Consolidated 2026-08-06; /api/formal-endorsement is deleted.
Three fields from the UN's reference form were dropped as unmodelled segmentation data: organisation type, country, and employee count.
- Admin: https://next.sarapis.org/admin
- Multi-brand. Every read/write is scoped by
NEXT_PUBLIC_SITE_KEY(defaultwegovnyc) against the Sites collection. src/lib/api.jscarries the full client inherited from the wegov.nyc marketing site. This site only usescreateSubmission()and onefetchAPI()call for the endorser wall; the rest is kept so the two codebases stay diff-able and a CMS-backed page needs no new plumbing.
| Variable | Required | Default | Purpose |
|---|---|---|---|
NEXT_PUBLIC_PAYLOAD_URL |
no | https://next.sarapis.org |
CMS origin |
NEXT_PUBLIC_SITE_KEY |
no | wegovnyc |
Brand scope in Payload |
There are no required secrets. ENDORSEMENT_SHEET_WEBHOOK_URL was removed on
2026-08-06 with the Google Sheet path — endorsements go to Payload, which needs no
credential beyond the public brand key.
Cascade layers, declared in src/app/base.css:
reset < components < unnyc < site
unnyc.cssandprimer.csswrap their rules in@layer unnyc.sitesits aboveunnycon purpose. The nav lives inside.unnyc-pageso it inherits that scope's tokens — which also means.unnyc-page a { color: inherit }would otherwise paint the nav links navy-on-navy. A later layer beats it with no specificity hacks.- Tokens come from
@wegovnyc/design-tokens, imported inbase.cssalong with theunnycbrand variant. Rules read the SEMANTIC tier (--wg-*) directly — the local--unnyc-*alias layer and the hand-copiedwegov-tokens.csswere both deleted on 2026-08-05. Never write a colour literal or read a reference token (--db-*) in a rule: both are invisible to the brand variant.npm run lint:tokenswarns if you do.
- Icons are one inline SVG set —
src/components/unnyc/UnnycIcon.js, paths taken verbatim from Lucide (ISC), a single 24×24 canvas at 2px stroke. Content refers to them by name (icon: shield-check). They took over from eight PNGs that were 364 KB in four clashing art styles and, more to the point, un-themeable — a black raster is a colour literal, invisible to the brand variant. Colour now comes fromcolor:on the CSS class. Add new icons from Lucide on the same canvas. - Photos live in
public/images/and are served throughnext/image— never a CSSbackground-image, which forfeits WebP, responsive sizing and lazy loading. public/images/CREDITS.mdrecords the source and licence of every image, and must be updated in the same commit that changes one. Where a licence requires attribution it also has to render on the page: the/resourcesOSPO figure is CC BY 4.0 and carries its citation in a figcaption. Check the licence before using anything from a publisher CDN — that figure is reusable only because its article is hybrid open access.
Pushing to main deploys to production. Git integration was connected on
2026-08-04, so a merge to main goes live at https://unnyc.wegov.nyc with no
manual step. PRs get preview deployments.
To force a deploy without a commit:
vercel deploy --prodA fresh clone must be linked first (.vercel/ is gitignored):
vercel link --yes --project unnyc-campaignVercel project: devins-projects-1baf43f0/unnyc-campaign (named unnyc-campaign
because the project name unnyc was already taken by the older Vite site, now at
old-unnyc.wegov.nyc).
Vercel's Hobby plan rejects git integration for private org-owned repos
(409, "Upgrade to Pro"); public org-owned repos are fine, which is why the sibling
wegovnyc_front has always auto-deployed. Rather than pay for Pro or maintain a
GitHub Actions workaround, the repo was made public — after auditing the history
for secrets (none: no .pem/.key/.env file has ever been committed, and
.env.example holds placeholders with an empty webhook value). Secret scanning
and push protection are enabled. Real secrets belong in Vercel env vars.
More than one Claude session gets run against this checkout, and on 2026-08-07 that cost real effort three times in one afternoon — a concurrent session's files turned up staged inside another session's commit, and one session committed on top of another's unpushed work so the two could not ship separately.
Never git add -A, git add . or git commit -a here. A PreToolUse hook
in .claude/settings.json refuses them
(scripts/hooks/no-blanket-git-staging.sh);
.gitignore has a deliberate .claude/* + !.claude/settings.json exception so
that guard travels with the repo. Stage by name, then read
git diff --cached --name-only before committing. For parallel work use a
worktree — see the CLAUDE.md section for the four gitignored things a new
worktree does not inherit.
Nothing outstanding. The campaign accepts signatures, organization endorsements and contact messages, and approved entries reach the public endorser wall.
Worth a decision rather than a fix: the CTFG map layer is now live, so the open question about linking into the Civic Tech Field Guide directory while it is de-indexed pre-launch is a current question, not a deferred one.
One thing to know rather than fix: published defaults to false, so a new
submission is invisible until someone ticks it in the Payload admin. That is the
review step, not a bug. If the wall looks empty, check there first.
-
Nothing could reach the endorser wall — four independent silent failures (all fixed 2026-08-06). Payload read was
authenticatedso anonymous reads 403'd (CMS r42); CORS lackedunnyc.wegov.nyc, so the browser blocked every submission before it was sent, introduced by this site's own extraction onto a new subdomain (CMS r43);fetchAPIhad no case forcampaign-endorsementsand fell through to an empty list; and/campaign-endorsements/statswas not a real endpoint. Each alone was enough to keep the wall empty, and none logged anything. Verified end to end through the live form. -
/campaign/endorsereturned 503 — the Google Sheet path needed anENDORSEMENT_SHEET_WEBHOOK_URLthat was never set. Resolved by deleting that path: endorsements go to Payload, which needs no secret. -
The eight Principles were listed in three places and had drifted.
content/principles.mdis now the single source; the letter and the printable declaration derive from it.src/data/is deleted. See docs/EDITING-CONTENT.md. -
wegov.nyc/unnyc/*now 308s to this site (wegovnyc_front@84a83de), so the duplicate no longer competes in search./unnyc/guide→/resources, since that article was never carried over here. -
Footer tagline is the campaign framing, reusing
content/home.md's ownogDescriptionso the two can't drift. -
Page H1s on
/startand/crosswalkdeliberately do not match their nav labels. Editorial headlines and short wayfinding labels are different jobs, and the H1s are the stronger copy. Only the<title>/ogTitlewere aligned, so a search result matches the label a visitor sees on arrival. Don't "fix" this.
Extracted from the wegov.nyc marketing site,
where this lived at /unnyc. All routes moved up one level (/unnyc/start →
/start), the CMS-driven marketing navbar was replaced with a static UNNYC nav,
and the theme switcher, blog, and frozen marketing pages were left behind.
Git history starts fresh here on purpose: the parent repo has two private SSH keys
in its public history, and rebasing onto it would have replicated that exposure.
Olivia Croteau's four-path restructure is credited via Co-Authored-By on the
initial commit.