8.5 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Overview
Church website for the Heilige Drei Könige Catholic parish, built on Next.js 15 (App Router) + React 19 + Payload CMS v3 + PostgreSQL/PostGIS. The frontend is fully server-rendered via Next; content is authored in the Payload admin and rendered from typed collection blocks. The repo is a multi-tenant template: one codebase serves multiple parish sites, selected at build/run time via NEXT_PUBLIC_SITE_ID.
Common commands
npm run dev # Next dev server (http://localhost:3000, admin at /admin)
npm run devsafe # Same but wipes .next first (use after Payload schema changes)
npm run build # Production build (output: 'standalone' for Docker)
npm start # Serve production build
npm run lint # Next/ESLint. Note: eslint.ignoreDuringBuilds is true in next.config
npm test # Vitest (run a single test: npx vitest run path/to/file.test.ts)
npm run storybook # Storybook on :6006 (see Storybook section for caveats)
# Payload
npm run payload migrate:create --name <name> # create a migration from current schema
npm run payload migrate # apply pending migrations
npm run generate:types # regenerate src/payload-types.ts
npm run payload jobs:run --cron "* * * * *" --queue default # run queued jobs locally
Node 22+ is required (engines). The predev/prebuild hooks run scripts/copy-favicon.mjs to copy the active site's icon.ico into src/app/(home)/.
Architecture
Multi-site configuration
sites/<siteId>/config.tsexports aSiteConfig(name, colors, fonts, contact info). Current sites:dreikoenige,chemnitz.src/config/site.tspicks one based onNEXT_PUBLIC_SITE_IDand exposessiteConfig. Unknown IDs throw at module load.- Each site also provides its own
Logo.tsx,logoSvg.ts, andicon.ico. When adding theming or site-specific assets, go throughsiteConfig— don't hardcode.
Next App Router layout
src/app/ uses two route groups:
(home)— public site. Notably:[[...slug]]/page.tsxis the catch-all that renders any CMSPagesdocument via the shared<Blocks>composition.- Dedicated routes exist for domain entities:
gemeinde/(churches),gruppe/(groups),gottesdienst/(worship),veranstaltungen/(events),blog/,pfarrei/(parish),sakramente/,spenden/(donations),suche/(search),gebetsanliegen-des-papstes/(pope's prayer intentions). api/draft/enables Payload draft mode for live preview.
(payload)— Payload admin (/admin) and Payload's own API routes. Don't edit files under this group by hand; they're scaffolded by@payloadcms/next.
Payload as the content layer
src/payload.config.ts is the single source of truth. Collections, globals, the Lexical editor config, the jobs queue, and plugins (GCS storage, search) all live there.
- Collections (
src/collections/*.ts):Parish,Churches,Worship,Events,Blog,Groups,Pages,Announcements,LiturgicalCalendar,PopesPrayerIntentions,Classifieds,ContactPerson,Locations,DonationForms,Prayers,Magazine,Highlight,Documents,Media,Users. - Globals (
src/globals/):Menu,Footer, plus aValidateHrefhelper. - Blocks (
src/collections/blocks/*.ts): reusable Lexical-adjacent block schemas referenced byPages.content,Blog.content, etc. Each block has a matching renderer branch insrc/compositions/Blocks/Blocks.tsx— adding a new block means updating both the schema andBlocks.tsx. - Types:
src/payload-types.tsis generated — never edit by hand. Runnpm run generate:typesafter schema changes. The TS path alias@payload-configresolves to the config file and@/*resolves tosrc/*. - DB: Postgres adapter with
idType: 'uuid'andpush: false(migrations only, no dev-time schema push). PostGIS extension is required becauseLocationsuses geo fields. - GraphQL is disabled. Use the REST/local API.
- Admin UI: German only (
@payloadcms/translations/languages/de); logo comes from the active site'sLogo.tsxviaserverProps.
Data fetching pattern
Server components read via helpers in src/fetch/ (e.g. fetchPageBySlug, fetchBlogBySlug, fetchChurchBySlug) rather than calling Payload directly from page files. These helpers accept a draft flag so draft mode works end-to-end. Authenticated previews use isAuthenticated() from src/utils/auth.ts plus the <RefreshRouteOnSave> client component.
Rendering structure
src/components/<Name>/— leaf/primitive UI (Button,Title,Container, …). Each folder usually contains the.tsx, astyles.module.scss, and a.stories.tsx.src/compositions/<Name>/— larger assemblies that combine components and sometimes fetch data (e.g.Blocks,PageHeader,Footer,ContactSection).src/pageComponents/<Entity>/— page-level components for specific routes (Home,Parish,Event,Worship,Search).src/utils/dto/— DTO mappers that normalize Payload shapes (e.g.transformGallery,getPhoto) before passing data into presentation components. When a block renderer needs data, prefer an existing DTO helper over inlining the reshape.
Recurring masses / jobs queue
See the README for the full walkthrough. Key points for code changes:
- The task is
src/jobs/generateRecurringMasses.ts; it's registered inpayload.config.tsunderjobs.tasks. autoRunonly fires whenNODE_ENV === 'production'(viashouldAutoRun). In dev, either trigger via the admin "Run now" button or thenpm run payload jobs:runCLI command.- Churches'
afterChangehook queues a regeneration whenever therecurringSchedulefield changes. The task only appends future occurrences — it will not overwrite existingWorshipdocs. Worshipdocs produced by the task are indistinguishable from hand-created ones; editors can still cancel/modify individual occurrences.
Search
The @payloadcms/plugin-search plugin indexes parish, pages, blog, event, group. Two non-default tweaks in payload.config.ts worth knowing about:
beforeSyncmapsoriginalDoc.name→searchDoc.titlebecauseParishandGroupsdon't have atitlefield.searchOverrides.fieldslifts thedoc.valuerelationship'smaxDepthto 1 so the results page (/suche) can read each referenced doc'sslugto build URLs.
Storage (GCS)
The gcsStorage plugin is registered unconditionally but enabled: !!process.env.GOOGLE_BUCKET. alwaysInsertFields: true keeps the collection schema (and therefore payload-types.ts / importMap.js) identical whether or not GCS is active — so you don't get spurious diffs switching between local-only dev and cloud dev. Public URLs are built manually via generateFileURL (https://storage.googleapis.com/<bucket>/<prefix>/<filename>).
Styling
- SCSS Modules (
*.module.scss) colocated with components. - Root-level
_template.scssand CSS variables fromsiteConfigdrive theming. next.config.mjspinsimages.remotePatternstostorage.googleapis.com/<GOOGLE_BUCKET>/**; local uploads are served from Next's default route.
Storybook
.storybook/main.ts contains a custom mockServerModules Vite plugin that stubs out payload and every @payloadcms/* import with a Proxy-based no-op module. This is intentional — components that incidentally import Payload utilities won't blow up the browser build. If you add a component that genuinely needs server-only behavior in a story, refactor the story to pass data as props rather than trying to unmock.
Migrations
Payload migrations live in src/migrations/. Every schema change should produce a new <timestamp>_<name>.ts + .json pair via npm run payload migrate:create. Production deploys must run npm run payload migrate against the prod DB (not yet wired into CD — see README todo).
Path aliases
@/*→src/*@payload-config→src/payload.config.ts
Things to avoid
- Don't edit
src/payload-types.ts— regenerate it. - Don't edit files under
src/app/(payload)/admin/orsrc/app/(payload)/api/— they're Payload-owned. - Don't add a new block schema without also adding its renderer branch in
src/compositions/Blocks/Blocks.tsx— the Pages route will silently drop unknown block types. - Don't rely on
push: truedev-mode schema sync; this project uses migrations exclusively.