Documentation

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:

SignalWhen it firesAllowed action fields
$pageviewA public route loads or changespage_path, source_path
copy_install_commandA visitor copies a supported install commandcommand, component, source_path
primary_link_clickA visitor follows a repository, docs, or components linkdestination, 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.

The gate is two-tier, split by what each provider writes to the visitor's device.

ProviderLoadsWhy
Vercel Analytics, Speed InsightsAlwaysCookieless, stores no identifier — no consent requirement, and gating them would blind Core Web Vitals on most traffic for no privacy gain
Google Analytics 4After opt-inSets its own _ga cookies and auto-collects page views
PostHogAfter opt-inPersists 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 RenderBlocks and Pages collection snippets
  • the expected install result

On this page