Add a module
A module is a kind of content with its own pages, such as news or jobs. Adding one touches a handful of files, each in one place; the type checker and the tests point at whatever is missing. Use src/modules/jobs and src/collections/Jobs as a model.
As an example, say you add events at /events (/da/arrangementer).
1. The collection #
src/collections/Events/index.ts, built from the shared pieces:
localizedSlug()for the per-language slug,publishedAtField()andhideFromListsField()for the sidebar, andmetaTab()for SEO (src/fields);teaserTab()(src/fields/teaser.ts) if editors should shape the event's card;hooks: { ...revalidateDocumentHooks() }(src/hooks/revalidateDocument.ts): refreshes the event's pages in every language, its index and its lists when it is published, and redirects an old slug;versions: { drafts: { autosave: … } }, like the other collections with pages;access: { read: authenticatedOrPublished, create: authenticated, update: authenticated, delete: authenticated }. The role rules narrow that per role by themselves.
Add it to the enabledCollections([...]) list in src/payload.config.ts.
2. The registry entry #
Add an entry to ENTRIES in src/registry/index.ts. Everything that builds addresses reads it: the catch-all route, link pickers, redirects, the sitemap and "Index page for".
{
slug: 'events',
label: 'Events',
route: { kind: 'segment', segment: { en: 'events', da: 'arrangementer', de: 'veranstaltungen' }, index: true },
linkable: true,
teaser: {
label: { en: 'Event', da: 'Arrangement', de: 'Veranstaltung' },
title: 'title',
summary: ['summary', 'meta.description'],
image: ['heroImage', 'meta.image'],
eyebrow: { date: 'startsAt' },
sortBy: 'startsAt',
},
},
route: asegmentper language,index: truefor a list at/events(later pages at/events/page/2),detail: falsefor documents shown only in the list,facetsfor a page per topic,feed: truefor an RSS feed,parentto nest under another collection.teaser: how cards read the collection's documents, so any card block can show them.
registryProblems() checks the entries (a unit test runs it): unique slugs, valid segments, no clash with a language code or the site's own routes.
3. The renderers #
src/modules/events/index.tsx exports a CollectionModule (src/modules/types.ts), made with defineModule: the document page (Detail), the index list (List), the index title, and optionally metadata, JSON-LD and the site header's style on the document's page (headerStyle: 'overlay' puts the header over a picture Detail opens with, edge to edge, as the catalog's product pages do). A collection shown only in its list uses defineIndexModule; a parent of nested documents uses defineParentModule. Read lists through cachedList and listTag (src/utilities/lists.ts), so publishing refreshes them.
Add one line to MODULES in src/modules/index.ts. Its type comes from the registry: a registered collection without a module fails the type check.
The route does the rest: finding the document, redirects between languages, drafts behind review links, the site's header and footer, hreflang and breadcrumbs.
4. The switch #
- In
src/registry/modules.ts, addeventstoOPTIONAL_MODULES: its label, its collections, its blocks (a list block, say) and its groups of interface text. - In
src/modules.config.ts, addevents: true. A module without a switch fails the type check. - If events are tagged with topics, add the module to
SHARED_COLLECTIONS.topics.
5. Interface text #
Add an events group of keys to src/messages/en.json, da.json and de.json (the same keys in all three; a test checks), and name it in the module's messages. Microcopy then gets an "Events" group that hides while the module is off. Read the texts with next-intl's getTranslations({ locale, namespace: 'events' }).
6. Demo content #
Add scripts/demo/modules/events.ts with a seedEvents function that writes a few published events in English and Danish (writeDoc in scripts/demo/write.ts). Call it from seedDemo in scripts/demo/index.ts, guarded by moduleEnabled('events'), and pass its references to scripts/demo/pages.ts if pages show events. tests/int/demoSeed.int.spec.ts fails while a registered collection has no published document in the demo.
7. Tests #
- Its own tests skip while it is off:
describe.skipIf(!moduleEnabled('events'))in Vitest,test.skip(!moduleEnabled('events'), …)in Playwright. tests/int/roles.int.spec.ts: add what visitors may do with events (events: ['read']) toPUBLIC. The test checks every collection for every role, and fails while visitors may do something it does not list.tests/int/modules.int.spec.ts: update the inline snapshots of collection and block slugs, and the module lists the test names.src/registry/modules.test.tsnames modules too.- A seed helper in
tests/helpersif the end-to-end tests or the block gallery need events.
Then run pnpm generate:types, pnpm generate:importmap, pnpm lint, pnpm typecheck, pnpm test:unit and pnpm test:int.
What you get for free #
Once the entry is in the registry, the module has: localized addresses and redirects between languages; the sitemap; link pickers and rich-text links; cards in every teaser block; "Index page for" on pages; the role rules; and switching the whole module off in one line. With drafts it also gets scheduled publishing, review links and ✨ Translate, and a localized layout of blocks gets block sync.