Contents

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() and hideFromListsField() for the sidebar, and metaTab() 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: a segment per language, index: true for a list at /events (later pages at /events/page/2), detail: false for documents shown only in the list, facets for a page per topic, feed: true for an RSS feed, parent to 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, add events to OPTIONAL_MODULES: its label, its collections, its blocks (a list block, say) and its groups of interface text.
  • In src/modules.config.ts, add events: 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']) to PUBLIC. 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.ts names modules too.
  • A seed helper in tests/helpers if 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.

These guides ship with Tolk Starter 0.6.0: buyers get the same pages in their repository, next to the code they describe.