church-website/CLAUDE.md
2026-04-17 09:57:58 +02:00

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.ts exports a SiteConfig (name, colors, fonts, contact info). Current sites: dreikoenige, chemnitz.
  • src/config/site.ts picks one based on NEXT_PUBLIC_SITE_ID and exposes siteConfig. Unknown IDs throw at module load.
  • Each site also provides its own Logo.tsx, logoSvg.ts, and icon.ico. When adding theming or site-specific assets, go through siteConfig — don't hardcode.

Next App Router layout

src/app/ uses two route groups:

  • (home) — public site. Notably:
    • [[...slug]]/page.tsx is the catch-all that renders any CMS Pages document 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 a ValidateHref helper.
  • Blocks (src/collections/blocks/*.ts): reusable Lexical-adjacent block schemas referenced by Pages.content, Blog.content, etc. Each block has a matching renderer branch in src/compositions/Blocks/Blocks.tsxadding a new block means updating both the schema and Blocks.tsx.
  • Types: src/payload-types.ts is generated — never edit by hand. Run npm run generate:types after schema changes. The TS path alias @payload-config resolves to the config file and @/* resolves to src/*.
  • DB: Postgres adapter with idType: 'uuid' and push: false (migrations only, no dev-time schema push). PostGIS extension is required because Locations uses geo fields.
  • GraphQL is disabled. Use the REST/local API.
  • Admin UI: German only (@payloadcms/translations/languages/de); logo comes from the active site's Logo.tsx via serverProps.

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, a styles.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 in payload.config.ts under jobs.tasks.
  • autoRun only fires when NODE_ENV === 'production' (via shouldAutoRun). In dev, either trigger via the admin "Run now" button or the npm run payload jobs:run CLI command.
  • Churches' afterChange hook queues a regeneration whenever the recurringSchedule field changes. The task only appends future occurrences — it will not overwrite existing Worship docs.
  • Worship docs produced by the task are indistinguishable from hand-created ones; editors can still cancel/modify individual occurrences.

The @payloadcms/plugin-search plugin indexes parish, pages, blog, event, group. Two non-default tweaks in payload.config.ts worth knowing about:

  1. beforeSync maps originalDoc.namesearchDoc.title because Parish and Groups don't have a title field.
  2. searchOverrides.fields lifts the doc.value relationship's maxDepth to 1 so the results page (/suche) can read each referenced doc's slug to 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.scss and CSS variables from siteConfig drive theming.
  • next.config.mjs pins images.remotePatterns to storage.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-configsrc/payload.config.ts

Things to avoid

  • Don't edit src/payload-types.ts — regenerate it.
  • Don't edit files under src/app/(payload)/admin/ or src/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: true dev-mode schema sync; this project uses migrations exclusively.