Contributing
How contributors should propose and ship new Payload Components.
Payload Components is open-source and community-first — adoption and contributions are the whole point.
Aim every contribution at making the install contract clearer, not just adding more UI. A component that installs cleanly and twice is worth more than a prettier block that drifts.
New component checklist
A component proposal should arrive complete — incomplete components don't ship:
A small, useful Payload block concept — narrow enough to verify, useful enough to install.
A block config with editor-friendly fields.
A frontend component that renders without extra app assumptions.
A manifest with files, fragments, dependencies, and sample content.
Install tests for both first install and repeated install.
Documentation explaining the target use case.
Attribution
If a component's layout is adapted from an external open-source design — for example tailark/blocks (MIT) — credit it in two places so provenance survives both in the repo and in what ships to consumers:
A source comment at the top of the component: // Layout adapted from tailark/blocks (MIT) — re-implemented as a Payload block. This ships with the file through the registry.
A docs footer at the end of the component's page: Layout adapted from [tailark/blocks](https://github.com/tailark/blocks) (MIT), retokenized onto the Payload Components design system.
Then record the upstream item and the pinned upstream commit in payload-components/PROVENANCE.md so future drift checks are a mechanical diff. Attribute only genuine layout adaptations — an independent take on a generic pattern (a plain hero, a card grid) is your own work and needs no credit.
Visual standards
Every block speaks one visual language — light shadcn monochrome with a single emerald accent — defined as tokens in src/app/globals.css. Two checks keep new components on it, and both run in the release gate:
Colours come from tokens, never the raw palette. Reach for bg-card,
text-muted-foreground, border-border/70, text-primary, text-brand —
not bg-blue-500, text-white, or a hardcoded text-[#0a0a0a] /
bg-[oklch(…)]. The keywords transparent / current / inherit and
var(--…)-based values are fine.
Radius and letter-spacing use the named tokens, not arbitrary values:
rounded-frame / rounded-panel / rounded-card / rounded-inset (plus
rounded-lg / rounded-full), and tracking-eyebrow / tracking-display /
tracking-heading — not rounded-[2rem] or tracking-[-0.06em]. Spacing and
font size stay on the default scale.
tests/int/visual-standards.int.spec.ts enforces this statically across payload-components/source (and holds the landing twins to the colour palette), while tests/e2e/components-visual.e2e.spec.ts snapshots every component to catch anything pixels can see. Need a new radius or tracking value? Add a token to globals.css instead of reaching for an arbitrary value — the guard then allows it everywhere.
Pure layout geometry — aspect-[4/3], grid-cols-[auto_1fr_auto],
w-[calc(…)] — is per-component and stays arbitrary by design; only colour,
radius, letter-spacing, spacing, and font-size are token-governed.
Anonymous analytics contract
The general site analytics is limited to three anonymous signals. Their action payloads are fixed:
| Signal | When it fires | Allowed action fields |
|---|---|---|
$pageview | A public route loads or changes | page_path, source_path |
copy_install_command | A visitor copies a supported install command | command, component, source_path |
primary_link_click | A visitor follows a repository, docs, or components link | destination, href, source_path |
These values come from committed routes, links, and install commands, not visitor
input. PostHog capture adds only $current_url, $pathname, and $lib to the
action fields, plus the public api_key and a first-party pseudonymous
distinct_id in the transport envelope. The request omits browser credentials.
Leaving NEXT_PUBLIC_POSTHOG_KEY unset disables PostHog capture for that
deployment.
The three signals above describe the events this repo emits. They are not the
whole picture of what runs in a visitor's browser: AnalyticsShell also mounts
Google Analytics 4 (googletagmanager.com/gtag/js) and Vercel Analytics plus
Speed Insights. GA4 auto-collects its own page views and sets its own cookies,
and both GA4 and PostHog are third-party processors. Keep this paragraph accurate
if that stack changes — tests/int/analytics-contract.int.spec.ts pins the
PostHog event payloads only and cannot catch an undisclosed provider.
Consent
The gate is two-tier, split by what each provider writes to the visitor's device.
| Provider | Loads | Why |
|---|---|---|
| Vercel Analytics, Speed Insights | Always | Cookieless, stores no identifier — no consent requirement, and gating them would blind Core Web Vitals on most traffic for no privacy gain |
| Google Analytics 4 | After opt-in | Sets its own _ga cookies and auto-collects page views |
| PostHog | After opt-in | Persists a pc_distinct_id in localStorage |
On a first visit there is no GA4 script, no PostHog beacon, and no stored
identifier. State lives in src/lib/consent.ts under the pc_consent
localStorage key, the banner is ConsentBanner, and /privacy documents the
split and can withdraw consent.
Keep the tiers straight when adding a provider: the question is not whether the data feels sensitive, it is whether the provider writes to the device. If it sets a cookie or stores an id, it goes behind the gate.
Three rules to preserve when touching any of this:
A privacy signal is a decision, not a prompt. Global Privacy Control or
Do Not Track resolves to denied and the banner never appears.
Withdrawal reloads the page, and deletes what the opt-in wrote. Unmounting
AnalyticsShell does not unload a third party that is already running —
gtag stays initialised on window and keeps collecting. So setConsent
reloads on granted → denied, clears pc_distinct_id, and expires GA's own
_ga / _ga_* cookies. Stopping collection while leaving the cookies in
place would keep the visitor identifiable on their next visit.
Tests grant consent explicitly. tests/e2e/consent.ts seeds
pc_consent=granted for the specs that assert on analytics or capture visual
baselines, so the banner stays out of snapshots. The undecided state is
covered by tests/e2e/consent.e2e.spec.ts, and the axe suites deliberately
run without consent so the banner is held to the same a11y bar.
PostHog capture is additionally limited to the production hostnames, so local dev and preview deploys never send even after opting in.
Never add user-entered content, secrets, account identity, install state, or cross-site profiling. A future event must name one user action, use a low-cardinality event name, and keep every property either enumerated or derived from a committed site route, link, or command. Do not send free text, query strings, raw referrers, timestamps, or arbitrary external URLs. Update this contract and focused analytics coverage in the same pull request whenever the event vocabulary or fields change.
Template showcase analytics has a separate typed template_* vocabulary in
src/lib/analytics.ts. It follows the same privacy and low-cardinality rules;
preview routes do not mount the general analytics stream.
Keep the catalog curated
The catalog grows by curation, not volume — every component must stay verifiable no matter how many ship.
A component is ready when it installs cleanly, can be run twice, and has docs good enough for another developer to reason about the target project changes.
Report install drift
If a supported Payload project shape changes, open an issue with enough context for the wrapper to update the support matrix — or reject the target shape clearly:
- Payload version
- Next.js version
- the failing command
- relevant
RenderBlocksandPagescollection snippets - the expected install result