fix(cms): enforce public publication contract

This commit is contained in:
syntaxbullet
2026-07-31 14:01:18 +02:00
parent 740e302c58
commit fec1bab2e5
4 changed files with 651 additions and 6 deletions

View File

@@ -0,0 +1,384 @@
# Client feedback implementation task list
This plan translates the client feedback from `Feedback.xlsx` into implementation tasks for the current Payload CMS and Next.js codebase.
## Confirmed decisions
- Unpublishing any draft-enabled CMS document must remove it from the anonymous public frontend.
- Authenticated draft/live preview may continue to show unpublished content intentionally.
- The CMS cleanup is focused: remove obsolete or non-rendered controls, keep page-specific content on its page, and keep content shared by multiple pages in Globals.
- `/netzwerk` receives its own CMS video upload instead of reusing the homepage video.
- Images and videos introduced by this work must be selectable through Payload's Media collection.
- Sponsor hierarchy needs a responsive design checkpoint before implementation.
## Definition of done
- An anonymous visitor cannot see an unpublished page, winner, jury member, partner, event, or press article in a list, relationship-driven section, direct detail route, sitemap, or fallback UI.
- An editor can still preview drafts through Payload preview/live preview.
- Payload exposes only controls that affect the current frontend or an explicitly retained global contract.
- New press and Netzwerk media can be selected and replaced in Payload without developer assistance.
- All visual feedback is verified at desktop, tablet, and mobile widths.
- Every schema change has a committed migration tested against a copy of the current SQLite database.
## Recommended implementation order
### 1. Establish the publication contract
This is the highest-priority work because later CMS cleanup must not hide the underlying visibility problem.
- [ ] Reproduce unpublishing from the Payload admin for every draft-enabled collection:
- [ ] `pages`
- [ ] `preistraeger`
- [ ] `jury-mitglieder`
- [ ] `partners`
- [ ] `events`
- [ ] `posts`
- [ ] For every reproduction, record:
- [ ] the document ID and `_status` before and after clicking **Unpublish**
- [ ] whether the admin request actually saves `_status = draft`
- [ ] behavior in an anonymous/private browser session
- [ ] behavior in Payload preview or a browser carrying a draft-mode cookie
- [ ] behavior on the collection/list page and direct detail URL
- [ ] Confirm that the reported problem is not only draft preview doing what it is designed to do.
- [ ] Add a table-driven integration test that queries each collection as an anonymous public request and proves that only `_status = published` documents are returned.
Acceptance criteria:
- [ ] Directly unpublishing a document changes its stored `_status` to `draft`.
- [ ] Anonymous Local API reads enforce access control with `overrideAccess: false` and `draft: false`.
- [ ] Draft preview is the only frontend context allowed to request unpublished documents.
### 2. Fix public route hydration and relationship resolution
The public route currently loads the primary document plus supporting collections in `src/app/(frontend)/[[...slug]]/page.tsx`. Make the public/draft boundary explicit rather than relying on implicit parameter combinations.
- [ ] Split public and preview query options into a small, testable helper:
- [ ] public: `draft: false`, `overrideAccess: false`
- [ ] preview: drafts enabled only when Next draft mode is active
- [ ] Apply the same contract to:
- [ ] the route document
- [ ] support pages
- [ ] winners
- [ ] events
- [ ] press articles
- [ ] partners
- [ ] jury members
- [ ] Ensure relationship-populated objects cannot bypass the filtered public collection.
- [ ] Update `resolveCmsRelationship(...)` and/or the consuming selection helpers so a relationship ID missing from the published collection resolves to nothing, never to the embedded relationship object.
- [ ] Keep the existing jury page selection authoritative, but silently omit selected jury members that are unpublished.
- [ ] Add regression tests for:
- [ ] unpublished jury member selected on `/netzwerk`
- [ ] unpublished winner selected on the homepage or `/der-bmp`
- [ ] unpublished partner previously present in the homepage ticker
- [ ] unpublished event previously present on `/presse`
- [ ] unpublished press article previously present on `/presse`
Acceptance criteria:
- [ ] Unpublishing one related item removes only that item and preserves the remaining published selection order.
- [ ] Republishing the item restores it without requiring a page document edit.
### 3. Remove collection fallbacks that resurrect unpublished content
Fallbacks for a missing image or missing optional field may remain. Entire fallback records must not replace intentionally empty CMS collections.
- [ ] Change `/presse` so an empty published `events` collection renders an intentional empty state or no event rows instead of `pressIndex.events.fallbackItems`.
- [ ] Change `/presse` so an empty published `posts` collection renders an intentional empty state or no news cards instead of `pressIndex.news.fallbackItems`.
- [ ] Remove the event and news fallback-record arrays from `pressIndexFields.ts` after the collection-backed render path is authoritative.
- [ ] Remove their preload/seed plumbing from `preload-press-index-page-cms.ts` and fresh-database setup.
- [ ] Audit winner, jury, and partner surfaces for the same pattern.
- [ ] Preserve only presentation fallbacks that cannot make a removed CMS record reappear, such as a placeholder image on an otherwise published document.
Acceptance criteria:
- [ ] Unpublishing the final event, article, or related record produces an honest empty state.
- [ ] No demo/default record appears merely because the CMS collection became empty.
### 4. Complete the focused Payload CMS cleanup
Create a field-to-renderer inventory before deleting schema. Every retained field must have a current frontend consumer or a documented global purpose.
#### Startseite
- [ ] Keep the page-specific hero, winners, quick check, intro, testimonials, benefits, and homepage video controls.
- [ ] Remove the hidden legacy `home.application` group now owned by the `application-form` Global.
- [ ] Remove the hidden legacy `home.form` group now owned by the `application-form` Global.
#### Teilnahme
- [ ] Keep hero, process, eligibility, application paths, dates, and awards.
- [ ] Remove the hidden legacy `participation.applicationForm` group.
- [ ] Remove the hidden legacy `participation.form` group.
#### Der BMP
- [ ] Remove the hidden legacy `about.highlights` group because the rendered winners section reuses the homepage source of truth.
#### Presse
- [ ] Remove obsolete press-kit controls that no longer render:
- [ ] `kitLabel`
- [ ] `kitMeta`
- [ ] `kitFile`
- [ ] `kitUrl`
- [ ] `kitDownloadName`
- [ ] Remove the complete non-rendered `pressIndex.accreditation` group.
- [ ] Remove the fallback-record arrays covered by Task 3.
- [ ] Keep the rendered section headings, descriptions, status labels, missing-image controls, and press-contact copy.
#### Netzwerk
- [ ] Remove confirmed non-rendered fields after the field-to-renderer audit, including current candidates:
- [ ] `jury.note`
- [ ] `partners.premiumLabel`
- [ ] `partners.defaultLogoColor`
- [ ] sponsoring membership-footnote fields
- [ ] Do not remove partner tier labels, partner modal copy, jury selection, or sponsoring form copy that still renders.
#### Global ownership
- [ ] Keep shared application content in **Formulare > Bewerbungsformular**.
- [ ] Keep phase state in the **Application Phase** Global.
- [ ] Keep phase-dependent newsletter content in the **Newsletter-Formular** Global.
- [ ] Keep Header, Footer, and site-wide settings in their existing Website Globals.
- [ ] Do not create a generic “Sections” content type unless a second real consumer justifies it.
#### Schema completion
- [ ] Generate a narrow migration that removes the obsolete live-table and version-table columns/child tables.
- [ ] Update `payload-types.ts`.
- [ ] Update preload, seed, and fresh-database scripts.
- [ ] Validate migration upgrade and rollback on a copied SQLite database.
Acceptance criteria:
- [ ] The Payload admin contains no hidden legacy groups or controls with no current frontend effect.
- [ ] Shared content is edited once in a Global and is not duplicated on individual pages.
- [ ] Page editors contain only page-specific content plus standard page metadata/SEO controls.
### 5. Give `/netzwerk` its own Ilse Aigner video
- [ ] Add a video upload field under the Netzwerk patronage/greeting section.
- [ ] Filter the upload relationship to video MIME types.
- [ ] Add page-specific video button label, title, description, and close-label fields if those strings need to differ from the homepage.
- [ ] Replace the `HomeVideoCms`/homepage lookup in `Netzwerk.tsx` with the Netzwerk page fields.
- [ ] Keep the existing homepage video and modal completely independent.
- [ ] Update preload/fresh-database code without copying the existing homepage media relationship into the new field unless the client explicitly wants a temporary placeholder.
- [ ] Let the client upload and select the supplied Netzwerk video after deployment.
Acceptance criteria:
- [ ] Changing the homepage video does not change `/netzwerk`.
- [ ] Changing the Netzwerk video does not change the homepage.
- [ ] Empty Netzwerk video selection hides the video action cleanly.
### 6. Add the CMS-controlled press-contact image
- [ ] Add `contactImage` and `contactImageAlt` to the rendered Presse contact/material section.
- [ ] Use a Payload upload relationship filtered to image MIME types.
- [ ] Restore the desktop contact section to a two-column composition with copy on the left and the selected image on the right.
- [ ] Stack image and copy in a deliberate order on mobile.
- [ ] Define a safe empty-image layout so the section still works before the client selects an asset.
- [ ] Update the preload script and migration.
Acceptance criteria:
- [ ] The client can select, replace, or clear the image in Payload.
- [ ] No code edit is required to change the press image.
### 7. Repair the `/teilnahme` “Schritt für Schritt” timeline
The current desktop implementation uses a `300vh` scroll container and produces overlapping/off-screen cards.
- [ ] Replace the current per-card transform calculation with a single deterministic horizontal-track transform, or replace scroll-jacking with an explicit horizontal carousel/stepper.
- [ ] Ensure steps 14 are all reachable at normal wheel/trackpad speed.
- [ ] Remove the unused vertical space after the final step.
- [ ] Make the progress indicator derive from actual scroll/active-step state instead of fixed copy.
- [ ] Preserve the existing stacked mobile presentation unless testing finds a separate defect.
- [ ] Respect `prefers-reduced-motion` by providing a non-scroll-jacked interaction.
- [ ] Add a browser regression test that proves every step becomes visible and the next section follows without a large blank gap.
Acceptance criteria:
- [ ] Steps 1, 2, 3, and 4 can each be read in full.
- [ ] Cards never overlap at supported desktop/tablet widths.
- [ ] The eligibility section begins promptly after the timeline finishes.
### 8. Design and approve a responsive sponsor hierarchy
Recommended direction: preserve a hierarchy through tier bands and decreasing visual weight rather than forcing lower tiers into empty higher-tier slots. Mixing tiers within one row weakens the pyramid and produces inconsistent card widths.
#### Proposed responsive behavior
- [ ] Desktop (`>= 1200px`):
- [ ] Tier 1: up to 2 large cards per centered row
- [ ] Tier 2: up to 3 medium cards per centered row
- [ ] Tier 3: up to 4 compact cards per centered row
- [ ] Center incomplete rows rather than filling them with a different tier
- [ ] Tablet (`7681199px`):
- [ ] Tier 1: 2 columns
- [ ] Tier 2: 2 columns
- [ ] Tier 3: 3 columns where space allows, otherwise 2
- [ ] Mobile (`< 768px`):
- [ ] one clearly labeled section per tier
- [ ] Tier 1 rendered as full-width feature cards
- [ ] Tier 2 rendered as medium full-width cards
- [ ] Tier 3 rendered as compact cards, using two columns only when logos remain readable
- [ ] Use card size, logo area, spacing, and tier heading—not color alone—to communicate rank accessibly.
- [ ] Keep `partner.tier` and `partner.sortOrder` as the CMS source of truth.
Approval gate:
- [ ] Produce desktop, tablet, and mobile mockups using the current partner counts.
- [ ] Review the hierarchy with the client before implementing the final grid.
- [ ] After approval, implement the chosen breakpoints without changing the partner data model unless the mockup reveals a real need.
Acceptance criteria:
- [ ] Hierarchy is immediately understandable at all three breakpoint classes.
- [ ] Adding or removing a partner does not create broken gaps or stretch a card unpredictably.
- [ ] Logo aspect ratios remain intact.
### 9. Make sponsor ordering explicit and manageable
- [ ] Confirm that `sortOrder` remains authoritative within each tier.
- [ ] Show `tier`, `sortOrder`, publication status, and active state in useful Partners admin columns.
- [ ] Add validation preventing unsupported tier values outside 13.
- [ ] Document the current order for client review:
1. RSM Ebner Stolz
2. WWK
3. Radio Gong 96.3
4. münchen.tv
5. METZLER
6. Dr. Wieselhuber & Partner
7. Fristads
8. New Edge
9. Moodtalk
10. Deutsche Bank
11. Bionorica
12. Primus
- [ ] Treat an order change as a CMS content operation, not a frontend code change.
### 10. Slow the homepage sponsor ticker
- [ ] Replace the fixed `22s` duration with a slower content-aware duration or an agreed slower fixed baseline.
- [ ] Keep perceived speed stable as the number of logos changes.
- [ ] Pause or substantially reduce motion on hover/focus and respect `prefers-reduced-motion`.
- [ ] Verify that duplicating the track still produces a seamless loop.
Acceptance criteria:
- [ ] The ticker feels calm enough to read individual logos.
- [ ] No jump is visible at the loop boundary.
### 11. Correct the homepage phase-heading line break
- [ ] Confirm the client copy belongs to the homepage status/phase module.
- [ ] Make phase headlines honor explicit CMS newlines.
- [ ] Store/render the target split as:
```text
Ein weiteres
erfolgreiches Jahr
```
- [ ] Preserve responsive wrapping when translated or edited copy is longer.
- [ ] Do not hard-code this one sentence in the component.
### 12. Adjust the homepage bottom application/newsletter split
- [ ] Widen the blue left panel on the homepage so the boundary with the gold form panel feels more balanced.
- [ ] Scope the ratio change through the existing `source="homepage"` prop unless the client separately approves the same change on `/teilnahme`.
- [ ] Recheck text measure, benefit rows, form width, and the image area after changing the ratio.
- [ ] Verify both open-application and newsletter phase variants.
### 13. Verify the shared application/newsletter background color
The current component already uses dark blue `#24366A` instead of black.
- [ ] Compare the deployed site with the current source.
- [ ] If production is still black, deploy the existing dark-blue implementation or identify the production content/style override.
- [ ] Make no additional palette change if production already matches the requested dark blue.
### 14. Improve `/teilnahme` eligibility hierarchy
- [ ] Keep the existing content and CTAs.
- [ ] Introduce clearer grouping within the white eligibility area through restrained section tinting, dividers, spacing, or row containers.
- [ ] Avoid turning every criterion into an unrelated floating card.
- [ ] Make the visual transition into “Wichtige Termine” clear without redesigning the rest of the page.
- [ ] Verify mobile scan order and contrast.
### 15. Adjust `/der-bmp` “Relevanz & Zielsetzung”
- [ ] Increase the desktop share of the left image so more of it is visible.
- [ ] Shift the right-hand copy farther into the viewport and give it a centered content measure within its panel.
- [ ] Tune the image `object-position` rather than cropping important subjects.
- [ ] Keep mobile image-first ordering and readable padding.
### 16. Correct `/der-bmp` “Was ist der Preis?” facts and caption
- [ ] Center the three key facts within the text panel instead of anchoring each value to the left edge.
- [ ] Keep separators and equal visual weight across all three facts.
- [ ] Improve the image-caption legibility with a controlled background/overlay or by moving it into a dedicated caption area.
- [ ] Verify long CMS labels do not collide.
### 17. Fix sitemap route generation
Sitemap support already exists, but current output needs correction.
- [ ] Generate the homepage URL as `/` when the page slug is either `home` or `startseite`, or prefer the stored `spaPath` directly.
- [ ] Generate press article URLs under `/presse/blog/<slug>` instead of `/posts/<slug>`.
- [ ] Add event detail URLs under `/presse/events/<slug>`.
- [ ] Add winner detail URLs under `/preistraeger/<slug>`.
- [ ] Keep unpublished documents out by using public access and `_status = published`.
- [ ] Ensure generated sitemap and robots URLs use the production base URL during deployment, never committed localhost/example URLs.
- [ ] Add route-level tests for root, page, event, article, and winner entries.
Acceptance criteria:
- [ ] Every sitemap URL returns `200` on production.
- [ ] Unpublished entries disappear from the sitemap immediately or after the documented cache/revalidation window.
### 18. Regression, migration, and deployment verification
- [ ] Run focused unit/integration tests for publication filtering, relationship resolution, and sitemap paths.
- [ ] Run TypeScript without incremental cache.
- [ ] Run the relevant Vitest suite.
- [ ] Build against a migrated copy of `content.db`.
- [ ] Visually verify at approximately `1440px`, `1024px`, `768px`, and `390px` widths.
- [ ] Test anonymous public browsing separately from authenticated draft preview.
- [ ] Back up the production SQLite database.
- [ ] Deploy the schema-changing commit with explicit migrations enabled.
- [ ] Upload/select the client-supplied Netzwerk video and the selected press image in production Payload.
- [ ] Smoke-test all affected routes after deployment:
- [ ] `/`
- [ ] `/teilnahme`
- [ ] `/der-bmp`
- [ ] `/netzwerk`
- [ ] `/presse`
- [ ] representative event/article/winner detail routes
- [ ] sitemap and robots endpoints
- [ ] Perform a production unpublish/republish smoke test using disposable or approved test records.
## Feedback traceability
| Workbook feedback | Planned work |
| --- | --- |
| CMS unpublish button does not work | Tasks 13 and 18 |
| `/teilnahme` steps 24 do not open; excess space | Task 7 |
| Separate Ilse Aigner video from homepage | Task 5 |
| Remove press-kit CMS controls | Task 4 |
| Keep Pages focused and shared content in Globals | Task 4 |
| Remove accreditation form copy | Task 4 |
| Shared 2027-interest section should be dark blue | Task 13; current source already appears compliant |
| `/der-bmp` relevance text/image alignment | Task 15 |
| Partner tier hierarchy and responsive layout | Tasks 89 |
| Add image beside press contact | Task 6 |
| Add/fix sitemap in code | Task 17 |
| Sponsor ticker feels too fast | Task 10 |
| Homepage success heading line break | Task 11 |
| Homepage bottom blue panel should be wider | Task 12 |
| Participation eligibility area lacks structure | Task 14 |
| `/der-bmp` facts and caption are misaligned/hard to read | Task 16 |
| Confirm sponsor order | Task 9 |
| “Aktuelles” looks good | No change required |