From 27b29a8a7a39434d3307f9a42382d6687e696ad0 Mon Sep 17 00:00:00 2001
From: syntaxbullet
Date: Mon, 22 Jun 2026 11:10:59 +0200
Subject: [PATCH] feat: manage press index via payload
---
package.json | 1 +
src/collections/Pages/index.ts | 19 +-
src/collections/Pages/pressIndexFields.ts | 152 +++++++++++++
src/payload-types.ts | 169 +++++++++++++++
src/scripts/preload-press-index-page-cms.ts | 60 +++++
src/spa/pages/Press.tsx | 229 +++++++++-----------
src/spa/pressIndexContent.ts | 100 +++++++++
7 files changed, 600 insertions(+), 130 deletions(-)
create mode 100644 src/collections/Pages/pressIndexFields.ts
create mode 100644 src/scripts/preload-press-index-page-cms.ts
create mode 100644 src/spa/pressIndexContent.ts
diff --git a/package.json b/package.json
index aff3bca..2cfc383 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"preload:impressum-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-impressum-page-cms.ts",
"preload:participation-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-participation-page-cms.ts",
"preload:preistraeger-index-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-preistraeger-index-page-cms.ts",
+ "preload:press-index-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-press-index-page-cms.ts",
"reinstall": "cross-env NODE_OPTIONS=--no-deprecation rm -rf node_modules && rm pnpm-lock.yaml && pnpm --ignore-workspace install",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
"test": "pnpm run test:int && pnpm run test:e2e",
diff --git a/src/collections/Pages/index.ts b/src/collections/Pages/index.ts
index 9cda35c..07582ab 100644
--- a/src/collections/Pages/index.ts
+++ b/src/collections/Pages/index.ts
@@ -15,6 +15,7 @@ import { homeFields } from './homeFields'
import { impressumFields } from './impressumFields'
import { participationFields } from './participationFields'
import { preistraegerIndexFields } from './preistraegerIndexFields'
+import { pressIndexFields } from './pressIndexFields'
import { slugField } from 'payload'
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
@@ -84,6 +85,14 @@ const isPreistraegerIndexPage = (_: unknown, siblingData?: { slug?: string; spaP
return spaPath === '/preistraeger' || slug === 'preistraeger' || title === 'preisträger' || title === 'preistraeger'
}
+const isPressIndexPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) => {
+ const slug = siblingData?.slug
+ const spaPath = siblingData?.spaPath
+ const title = siblingData?.title?.toLowerCase()
+
+ return spaPath === '/presse' || slug === 'presse' || slug === 'press' || title === 'presse' || title === 'press'
+}
+
const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: string; title?: string }) =>
isHomePage(undefined, siblingData) ||
isContactPage(undefined, siblingData) ||
@@ -91,7 +100,8 @@ const isManagedSpaPage = (_: unknown, siblingData?: { slug?: string; spaPath?: s
isAboutPage(undefined, siblingData) ||
isImpressumPage(undefined, siblingData) ||
isDatenschutzPage(undefined, siblingData) ||
- isPreistraegerIndexPage(undefined, siblingData)
+ isPreistraegerIndexPage(undefined, siblingData) ||
+ isPressIndexPage(undefined, siblingData)
export const Pages: CollectionConfig<'pages'> = {
slug: 'pages',
@@ -208,6 +218,13 @@ export const Pages: CollectionConfig<'pages'> = {
fields: preistraegerIndexFields,
label: 'Preisträger Index Page',
},
+ {
+ admin: {
+ condition: (data) => isPressIndexPage(undefined, data),
+ },
+ fields: pressIndexFields,
+ label: 'Press Index Page',
+ },
{
name: 'meta',
label: 'SEO',
diff --git a/src/collections/Pages/pressIndexFields.ts b/src/collections/Pages/pressIndexFields.ts
new file mode 100644
index 0000000..da469d5
--- /dev/null
+++ b/src/collections/Pages/pressIndexFields.ts
@@ -0,0 +1,152 @@
+import type { Field } from 'payload'
+
+import { pressIndexContent } from '@/spa/pressIndexContent'
+
+const uploadField = (name: string, label: string, description?: string): Field => ({
+ name,
+ type: 'upload',
+ relationTo: 'media',
+ label,
+ admin: description ? { description } : undefined,
+})
+
+const text = (name: string, label: string, defaultValue?: string): Field => ({
+ name,
+ type: 'text',
+ label,
+ defaultValue,
+})
+
+const textarea = (name: string, label: string, defaultValue?: string): Field => ({
+ name,
+ type: 'textarea',
+ label,
+ defaultValue,
+})
+
+const sectionAdmin = (description: string) => ({
+ description,
+ initCollapsed: true,
+})
+
+const eventFields: Field[] = [
+ text('title', 'Title'),
+ text('date', 'Date'),
+ text('location', 'Location'),
+ text('cat', 'Category'),
+ text('status', 'Status'),
+ text('img', 'Image URL or path'),
+ textarea('desc', 'Description'),
+ text('slug', 'Route'),
+]
+
+const newsFields: Field[] = [
+ text('title', 'Title'),
+ textarea('excerpt', 'Excerpt'),
+ text('cat', 'Category'),
+ text('img', 'Image URL or path'),
+ text('slug', 'Route'),
+]
+
+export const pressIndexFields: Field[] = [
+ {
+ name: 'pressIndex',
+ label: 'Press index page content',
+ type: 'group',
+ admin: {
+ description:
+ 'Edit the Presse listing page chrome. Event and news records are managed through Events and Presse collections; fallback cards remain editable here.',
+ },
+ fields: [
+ {
+ name: 'hero',
+ label: '01 · Hero',
+ type: 'group',
+ admin: sectionAdmin('Hero image, eyebrow, heading, and intro copy.'),
+ fields: [
+ uploadField('image', 'Hero image', `Current frontend image: /images/${pressIndexContent.hero.imageFilename}`),
+ text('imageAlt', 'Image alt text', pressIndexContent.hero.imageAlt),
+ text('eyebrow', 'Eyebrow', pressIndexContent.hero.eyebrow),
+ textarea('heading', 'Heading', pressIndexContent.hero.heading),
+ textarea('description', 'Description', pressIndexContent.hero.description),
+ ],
+ },
+ {
+ name: 'events',
+ label: '02 · Events section',
+ type: 'group',
+ admin: sectionAdmin('Section header, labels, and fallback event rows.'),
+ fields: [
+ text('eyebrow', 'Eyebrow', pressIndexContent.events.eyebrow),
+ textarea('heading', 'Heading', pressIndexContent.events.heading),
+ textarea('description', 'Description', pressIndexContent.events.description),
+ text('detailLabel', 'Detail link label', pressIndexContent.events.detailLabel),
+ text('openStatusLabel', 'Open status label', pressIndexContent.events.openStatusLabel),
+ {
+ name: 'fallbackItems',
+ label: 'Fallback event rows',
+ type: 'array',
+ dbName: 'press_events_fb',
+ defaultValue: pressIndexContent.events.fallbackItems,
+ fields: eventFields,
+ },
+ ],
+ },
+ {
+ name: 'news',
+ label: '03 · Newsroom section',
+ type: 'group',
+ admin: sectionAdmin('Section header, read-more label, and fallback news cards.'),
+ fields: [
+ text('eyebrow', 'Eyebrow', pressIndexContent.news.eyebrow),
+ textarea('heading', 'Heading', pressIndexContent.news.heading),
+ textarea('description', 'Description', pressIndexContent.news.description),
+ text('readMoreLabel', 'Read more label', pressIndexContent.news.readMoreLabel),
+ {
+ name: 'fallbackItems',
+ label: 'Fallback news cards',
+ type: 'array',
+ dbName: 'press_news_fb',
+ defaultValue: pressIndexContent.news.fallbackItems,
+ fields: newsFields,
+ },
+ ],
+ },
+ {
+ name: 'downloads',
+ label: '04 · Press material and contact',
+ type: 'group',
+ admin: sectionAdmin('Download button and press contact block.'),
+ fields: [
+ text('eyebrow', 'Eyebrow', pressIndexContent.downloads.eyebrow),
+ textarea('heading', 'Heading', pressIndexContent.downloads.heading),
+ textarea('description', 'Description', pressIndexContent.downloads.description),
+ text('kitLabel', 'Kit label', pressIndexContent.downloads.kitLabel),
+ text('kitMeta', 'Kit metadata', pressIndexContent.downloads.kitMeta),
+ text('kitUrl', 'Kit URL', pressIndexContent.downloads.kitUrl),
+ text('kitDownloadName', 'Kit download filename', pressIndexContent.downloads.kitDownloadName),
+ text('contactEyebrow', 'Contact eyebrow', pressIndexContent.downloads.contactEyebrow),
+ text('contactName', 'Contact name', pressIndexContent.downloads.contactName),
+ textarea('contactLines', 'Contact lines', pressIndexContent.downloads.contactLines),
+ ],
+ },
+ {
+ name: 'accreditation',
+ label: '05 · Accreditation form copy',
+ type: 'group',
+ admin: sectionAdmin('Labels and success state for the press accreditation form.'),
+ fields: [
+ text('eyebrow', 'Eyebrow', pressIndexContent.accreditation.eyebrow),
+ textarea('heading', 'Heading', pressIndexContent.accreditation.heading),
+ textarea('description', 'Description', pressIndexContent.accreditation.description),
+ text('mediumLabel', 'Medium label', pressIndexContent.accreditation.mediumLabel),
+ text('nameLabel', 'Name label', pressIndexContent.accreditation.nameLabel),
+ text('emailLabel', 'Email label', pressIndexContent.accreditation.emailLabel),
+ text('submitLabel', 'Submit label', pressIndexContent.accreditation.submitLabel),
+ text('successHeading', 'Success heading', pressIndexContent.accreditation.successHeading),
+ textarea('successMessage', 'Success message', pressIndexContent.accreditation.successMessage),
+ ],
+ },
+ ],
+ },
+]
diff --git a/src/payload-types.ts b/src/payload-types.ts
index 74a9bfd..b0a6eba 100644
--- a/src/payload-types.ts
+++ b/src/payload-types.ts
@@ -1242,6 +1242,95 @@ export interface Page {
};
};
};
+ /**
+ * Edit the Presse listing page chrome. Event and news records are managed through Events and Presse collections; fallback cards remain editable here.
+ */
+ pressIndex?: {
+ /**
+ * Hero image, eyebrow, heading, and intro copy.
+ */
+ hero?: {
+ /**
+ * Current frontend image: /images/gala-dinner.jpg
+ */
+ image?: (number | null) | Media;
+ imageAlt?: string | null;
+ eyebrow?: string | null;
+ heading?: string | null;
+ description?: string | null;
+ };
+ /**
+ * Section header, labels, and fallback event rows.
+ */
+ events?: {
+ eyebrow?: string | null;
+ heading?: string | null;
+ description?: string | null;
+ detailLabel?: string | null;
+ openStatusLabel?: string | null;
+ fallbackItems?:
+ | {
+ title?: string | null;
+ date?: string | null;
+ location?: string | null;
+ cat?: string | null;
+ status?: string | null;
+ img?: string | null;
+ desc?: string | null;
+ slug?: string | null;
+ id?: string | null;
+ }[]
+ | null;
+ };
+ /**
+ * Section header, read-more label, and fallback news cards.
+ */
+ news?: {
+ eyebrow?: string | null;
+ heading?: string | null;
+ description?: string | null;
+ readMoreLabel?: string | null;
+ fallbackItems?:
+ | {
+ title?: string | null;
+ excerpt?: string | null;
+ cat?: string | null;
+ img?: string | null;
+ slug?: string | null;
+ id?: string | null;
+ }[]
+ | null;
+ };
+ /**
+ * Download button and press contact block.
+ */
+ downloads?: {
+ eyebrow?: string | null;
+ heading?: string | null;
+ description?: string | null;
+ kitLabel?: string | null;
+ kitMeta?: string | null;
+ kitUrl?: string | null;
+ kitDownloadName?: string | null;
+ contactEyebrow?: string | null;
+ contactName?: string | null;
+ contactLines?: string | null;
+ };
+ /**
+ * Labels and success state for the press accreditation form.
+ */
+ accreditation?: {
+ eyebrow?: string | null;
+ heading?: string | null;
+ description?: string | null;
+ mediumLabel?: string | null;
+ nameLabel?: string | null;
+ emailLabel?: string | null;
+ submitLabel?: string | null;
+ successHeading?: string | null;
+ successMessage?: string | null;
+ };
+ };
meta?: {
title?: string | null;
/**
@@ -3122,6 +3211,86 @@ export interface PagesSelect {
};
};
};
+ pressIndex?:
+ | T
+ | {
+ hero?:
+ | T
+ | {
+ image?: T;
+ imageAlt?: T;
+ eyebrow?: T;
+ heading?: T;
+ description?: T;
+ };
+ events?:
+ | T
+ | {
+ eyebrow?: T;
+ heading?: T;
+ description?: T;
+ detailLabel?: T;
+ openStatusLabel?: T;
+ fallbackItems?:
+ | T
+ | {
+ title?: T;
+ date?: T;
+ location?: T;
+ cat?: T;
+ status?: T;
+ img?: T;
+ desc?: T;
+ slug?: T;
+ id?: T;
+ };
+ };
+ news?:
+ | T
+ | {
+ eyebrow?: T;
+ heading?: T;
+ description?: T;
+ readMoreLabel?: T;
+ fallbackItems?:
+ | T
+ | {
+ title?: T;
+ excerpt?: T;
+ cat?: T;
+ img?: T;
+ slug?: T;
+ id?: T;
+ };
+ };
+ downloads?:
+ | T
+ | {
+ eyebrow?: T;
+ heading?: T;
+ description?: T;
+ kitLabel?: T;
+ kitMeta?: T;
+ kitUrl?: T;
+ kitDownloadName?: T;
+ contactEyebrow?: T;
+ contactName?: T;
+ contactLines?: T;
+ };
+ accreditation?:
+ | T
+ | {
+ eyebrow?: T;
+ heading?: T;
+ description?: T;
+ mediumLabel?: T;
+ nameLabel?: T;
+ emailLabel?: T;
+ submitLabel?: T;
+ successHeading?: T;
+ successMessage?: T;
+ };
+ };
meta?:
| T
| {
diff --git a/src/scripts/preload-press-index-page-cms.ts b/src/scripts/preload-press-index-page-cms.ts
new file mode 100644
index 0000000..82b6966
--- /dev/null
+++ b/src/scripts/preload-press-index-page-cms.ts
@@ -0,0 +1,60 @@
+import 'dotenv/config'
+
+import config from '@payload-config'
+import { getPayload, type Payload } from 'payload'
+
+import { pressIndexContent } from '@/spa/pressIndexContent'
+
+const mediaByFilename = async (payload: Payload, filename: string) => {
+ const result = await payload.find({
+ collection: 'media',
+ limit: 1,
+ pagination: false,
+ where: { filename: { equals: filename } },
+ })
+ return result.docs[0]?.id
+}
+
+async function main() {
+ const payload = await getPayload({ config })
+
+ const pageResult = await payload.find({
+ collection: 'pages',
+ limit: 1,
+ pagination: false,
+ draft: true,
+ overrideAccess: true,
+ where: {
+ or: [{ spaPath: { equals: '/presse' } }, { slug: { equals: 'presse' } }, { slug: { equals: 'press' } }],
+ },
+ })
+
+ const page = pageResult.docs[0]
+ if (!page) throw new Error('Press index page not found. Expected spaPath=/presse or slug=presse/press.')
+
+ const heroImage = await mediaByFilename(payload, pressIndexContent.hero.imageFilename)
+ const { imageFilename: _heroFilename, ...heroContent } = pressIndexContent.hero
+
+ await payload.update({
+ collection: 'pages',
+ id: page.id,
+ overrideAccess: true,
+ context: { disableRevalidate: true },
+ data: {
+ pressIndex: {
+ ...pressIndexContent,
+ hero: {
+ ...heroContent,
+ image: heroImage,
+ },
+ },
+ } as never,
+ })
+
+ payload.logger.info(`Preloaded Press index CMS fields for page ${page.id}`)
+}
+
+main().catch((error) => {
+ console.error(error)
+ process.exit(1)
+})
diff --git a/src/spa/pages/Press.tsx b/src/spa/pages/Press.tsx
index ded9cbc..23ace13 100644
--- a/src/spa/pages/Press.tsx
+++ b/src/spa/pages/Press.tsx
@@ -3,9 +3,10 @@ import { Link } from '@/spa/router';
import { Calendar, MapPin, Download, ArrowRight, FileText } from 'lucide-react';
import MunichSkylineBg from '@/spa/components/ui/munich-skyline-bg';
import { useIsMobile } from '@/spa/hooks/useIsMobile';
-import { useCmsCollection } from '@/spa/cmsRoute';
-import { docImageUrl } from '@/spa/cmsMediaField';
+import { useCmsCollection, useCmsRoute, type CmsRouteDoc } from '@/spa/cmsRoute';
+import { docImageUrl, mediaAlt, mediaUrl } from '@/spa/cmsMediaField';
import Image from '@/spa/components/ui/UnoptimizedImage'
+import { pressIndexContent } from '@/spa/pressIndexContent';
const NAVY = '#111D55';
const GOLD = '#EFBF04';
@@ -13,67 +14,52 @@ const CREAM = '#E4E2E3';
const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif';
-const events = [
- {
- title: 'Preisverleihung 2026',
- date: '22. Okt 2026',
- location: 'München, Residenz',
- cat: 'Gala / Event',
- status: 'In Planung',
- img: '/images/buehne-moderatoren.jpg',
- desc: 'Der glanzvolle Höhepunkt des Jahres. Die Verleihung der Preise in der prachtvollen Kulisse der Residenz München.',
- slug: '/presse/events/preisverleihung-2026',
- },
- {
- title: 'Mittelstands-Gipfel',
- date: '15. Juni 2026',
- location: 'Nürnberg',
- cat: 'Workshop',
- status: 'Anmeldung offen',
- img: '/images/networking-innenhof.jpg',
- desc: 'Regionaler Austausch und Best-Practices für bayerische Unternehmen auf dem Weg zum Preis.',
- slug: '/presse/events/mittelstands-gipfel-2026',
- },
- {
- title: 'Nominierten-Auswahl',
- date: '04. Aug 2026',
- location: 'Regensburg',
- cat: 'Jury-Sitzung',
- status: 'Intern',
- img: '/images/saal-gedeckt.jpg',
- desc: 'Das Gremium sichtet die Ergebnisse der Audits und legt die Nominierten für die Hauptpreise fest.',
- slug: '/presse/events/nominierten-auswahl-2026',
- },
-];
+type PressIndexCms = Partial & {
+ hero?: Partial & { image?: unknown }
+}
-const news = [
- {
- title: 'Wie Bayerns KMU die KI nutzen',
- excerpt:
- 'Ein Deep-Dive in die Bewerbungsunterlagen 2024 zeigt: Der Mittelstand treibt die Digitalisierung aktiv voran.',
- cat: 'Innovation',
- img: '/images/gala-saal-overview.jpg',
- slug: '/presse/blog/ki-nutzung-bayerischer-kmu',
- },
- {
- title: 'Start der Ehrenamts-Initiative',
- excerpt:
- 'Gemeinsam mit unseren Partnern fördern wir soziale Projekte mittelständischer Unternehmen.',
- cat: 'Engagement',
- img: '/images/networking-innenhof.jpg',
- slug: '/presse/blog/ehrenamts-initiative-2026',
- },
- {
- title: 'Bayerische Wirtschaft wächst',
- excerpt:
- 'Neue Prognosen zeigen ein stabiles Wachstum für den Mittelstand – gute Aussichten für die Awards.',
- cat: 'Wirtschaft',
- img: '/images/gala-dinner.jpg',
- slug: '/presse/blog/bayerische-wirtschaft-waechst',
- },
-];
+type PressEventItem = (typeof pressIndexContent.events.fallbackItems)[number]
+type PressNewsItem = (typeof pressIndexContent.news.fallbackItems)[number]
-function NewsCard({ item, idx, isMobile }: { item: typeof news[0]; idx: number; isMobile: boolean }) {
+const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
+const fallbackArray = (value: unknown, fallback: T[]) => Array.isArray(value) && value.length ? value as T[] : fallback;
+
+const statusLabels: Record = {
+ geplant: 'In Planung',
+ 'anmeldung-offen': 'Anmeldung offen',
+ intern: 'Intern',
+ abgeschlossen: 'Abgeschlossen',
+}
+
+const normalizeStatus = (status: unknown) => {
+ const value = fallbackText(status, 'Geplant')
+ return statusLabels[value] || value
+}
+
+const eventFromCms = (event: CmsRouteDoc): PressEventItem => ({
+ title: String(event.title || 'Event'),
+ date: String(event.date || 'Termin folgt'),
+ location: String(event.location || event.venue || 'Bayern'),
+ cat: String(event.category || 'Event'),
+ status: normalizeStatus(event.status),
+ img: docImageUrl(event, '/images/buehne-moderatoren.jpg'),
+ desc: String(event.description || event.meta?.description || ''),
+ slug: String(event.spaPath || `/presse/events/${event.slug}`),
+})
+
+const postFromCms = (post: CmsRouteDoc): PressNewsItem => ({
+ title: String(post.title || 'Presse'),
+ excerpt: String(post.excerpt || post.meta?.description || ''),
+ cat: String(post.cat || 'Presse'),
+ img: docImageUrl(post, '/images/gala-saal-overview.jpg', 'heroImage'),
+ slug: String(post.spaPath || `/presse/blog/${post.slug}`),
+})
+
+function Lines({ text }: { text: string }) {
+ return <>{text.split('\n').map((line, i) => {i > 0 && }{line} )}>
+}
+
+function NewsCard({ item, idx, isMobile, readMoreLabel, total }: { item: PressNewsItem; idx: number; isMobile: boolean; readMoreLabel: string; total: number }) {
const [hovered, setHovered] = React.useState(false);
return (
0 ? '1px solid rgba(255,255,255,0.07)' : 'none'),
- borderBottom: isMobile && idx < news.length - 1 ? '1px solid rgba(255,255,255,0.07)' : 'none',
+ borderBottom: isMobile && idx < total - 1 ? '1px solid rgba(255,255,255,0.07)' : 'none',
textDecoration: 'none',
}}
>
@@ -177,7 +163,7 @@ function NewsCard({ item, idx, isMobile }: { item: typeof news[0]; idx: number;
gap: 5,
}}
>
- Weiterlesen
+ {readMoreLabel}
@@ -217,41 +203,23 @@ function DarkInput(props: React.InputHTMLAttributes) {
const Press: React.FC = () => {
const isMobile = useIsMobile();
+ const cms = (useCmsRoute()?.doc?.pressIndex || {}) as PressIndexCms;
+ const hero = { ...pressIndexContent.hero, ...(cms.hero || {}) };
+ const eventsSection = { ...pressIndexContent.events, ...(cms.events || {}) };
+ const newsSection = { ...pressIndexContent.news, ...(cms.news || {}) };
+ const downloads = { ...pressIndexContent.downloads, ...(cms.downloads || {}) };
+ const accreditation = { ...pressIndexContent.accreditation, ...(cms.accreditation || {}) };
+ const contactLines = fallbackText(downloads.contactLines, pressIndexContent.downloads.contactLines).split('\n');
const cmsEvents = useCmsCollection('events');
const cmsPosts = useCmsCollection('posts');
const combinedEvents = React.useMemo(() => {
- const existing = new Set(events.map((event) => event.slug.split('/').pop()));
- return [
- ...events,
- ...cmsEvents
- .filter((event) => event.slug && !existing.has(String(event.slug)))
- .map((event) => ({
- title: String(event.title || 'Event'),
- date: String(event.date || 'Termin folgt'),
- location: String(event.location || event.venue || 'Bayern'),
- cat: String(event.category || 'Event'),
- status: String(event.status || 'Geplant'),
- img: docImageUrl(event, '/images/buehne-moderatoren.jpg'),
- desc: String(event.description || event.meta?.description || ''),
- slug: String(event.spaPath || `/presse/events/${event.slug}`),
- })),
- ];
- }, [cmsEvents]);
+ if (cmsEvents.length) return cmsEvents.map(eventFromCms);
+ return fallbackArray(eventsSection.fallbackItems, pressIndexContent.events.fallbackItems);
+ }, [cmsEvents, eventsSection.fallbackItems]);
const combinedNews = React.useMemo(() => {
- const existing = new Set(news.map((item) => item.slug.split('/').pop()));
- return [
- ...news,
- ...cmsPosts
- .filter((post) => post.slug && !existing.has(String(post.slug)))
- .map((post) => ({
- title: String(post.title || 'Presse'),
- excerpt: String(post.excerpt || post.meta?.description || ''),
- cat: String(post.cat || 'Presse'),
- img: docImageUrl(post, '/images/gala-saal-overview.jpg', 'heroImage'),
- slug: String(post.spaPath || `/presse/blog/${post.slug}`),
- })),
- ];
- }, [cmsPosts]);
+ if (cmsPosts.length) return cmsPosts.map(postFromCms);
+ return fallbackArray(newsSection.fallbackItems, pressIndexContent.news.fallbackItems);
+ }, [cmsPosts, newsSection.fallbackItems]);
const [hoveredEvent, setHoveredEvent] = useState(null);
const [dlHovered, setDlHovered] = useState(false);
const [submitHovered, setSubmitHovered] = useState(false);
@@ -283,8 +251,8 @@ const Press: React.FC = () => {
}}
>
{
color: GOLD,
}}
>
- Presse & Newsroom
+ {fallbackText(hero.eyebrow, pressIndexContent.hero.eyebrow)}
{
overflowWrap: 'anywhere',
}}
>
- EVENTS & BERICHTERSTATTUNG.
+
{
margin: 0,
}}
>
- Alle Termine, Pressemitteilungen und Medienmaterial rund um den Bayerischen Mittelstandspreis.
+ {fallbackText(hero.description, pressIndexContent.hero.description)}
@@ -380,7 +348,7 @@ const Press: React.FC = () => {
marginBottom: 16,
}}
>
- Termine 2026
+ {fallbackText(eventsSection.eyebrow, pressIndexContent.events.eyebrow)}
{
margin: 0,
}}
>
- EVENTS RUND UM DEN PREIS.
+
@@ -406,7 +374,7 @@ const Press: React.FC = () => {
margin: 0,
}}
>
- Von der Jurysitzung bis zur festlichen Gala – alle Veranstaltungen auf einen Blick.
+ {fallbackText(eventsSection.description, pressIndexContent.events.description)}
@@ -429,7 +397,7 @@ const Press: React.FC = () => {
{/* Left – image cell */}
-
@@ -449,7 +417,7 @@ const Press: React.FC = () => {
{event.desc}
- Details
+ {fallbackText(eventsSection.detailLabel, pressIndexContent.events.detailLabel)}
@@ -481,7 +449,7 @@ const Press: React.FC = () => {
marginBottom: 16,
}}
>
- Newsroom
+ {fallbackText(newsSection.eyebrow, pressIndexContent.news.eyebrow)}
{
margin: 0,
}}
>
- NACHRICHTEN & EINBLICKE.
+
@@ -507,7 +475,7 @@ const Press: React.FC = () => {
margin: 0,
}}
>
- Berichte, Hintergründe und Einblicke rund um den bayerischen Mittelstand und den BMP.
+ {fallbackText(newsSection.description, pressIndexContent.news.description)}
@@ -515,7 +483,7 @@ const Press: React.FC = () => {
{/* 3-column news grid */}
{combinedNews.map((item, idx) => (
-
+
))}
@@ -547,7 +515,7 @@ const Press: React.FC = () => {
marginBottom: 16,
}}
>
- Pressekontakt & Material
+ {fallbackText(downloads.eyebrow, pressIndexContent.downloads.eyebrow)}
{
margin: '0 0 20px',
}}
>
- PRESSE-MATERIAL & KONTAKT.
+
{/* Gold divider */}
@@ -577,13 +545,13 @@ const Press: React.FC = () => {
marginBottom: 48,
}}
>
- Laden Sie unser offizielles Material herunter oder kontaktieren Sie direkt unser Presseteam für individuelle Anfragen.
+ {fallbackText(downloads.description, pressIndexContent.downloads.description)}
{/* Download button */}
setDlHovered(true)}
onMouseLeave={() => setDlHovered(false)}
style={{
@@ -622,10 +590,10 @@ const Press: React.FC = () => {
marginBottom: 3,
}}
>
- Download Presse-Kit
+ {fallbackText(downloads.kitLabel, pressIndexContent.downloads.kitLabel)}
- Print_BMP.zip – 1,5 MB
+ {fallbackText(downloads.kitMeta, pressIndexContent.downloads.kitMeta)}
@@ -643,7 +611,7 @@ const Press: React.FC = () => {
marginBottom: 12,
}}
>
- Pressekontakt
+ {fallbackText(downloads.contactEyebrow, pressIndexContent.downloads.contactEyebrow)}
{
marginBottom: 4,
}}
>
- Tanja Meier
+ {fallbackText(downloads.contactName, pressIndexContent.downloads.contactName)}
{
overflowWrap: 'anywhere',
}}
>
- presse@bmp-bayern.de
-
- +49 89 123 456 99
+ {contactLines.map((line, index) => (
+
+ {line}
+ {index < contactLines.length - 1 && }
+
+ ))}
@@ -717,7 +688,7 @@ const Press: React.FC = () => {
margin: '0 0 16px',
}}
>
- Anfrage eingegangen
+ {fallbackText(accreditation.successHeading, pressIndexContent.accreditation.successHeading)}
{
margin: 0,
}}
>
- Vielen Dank für Ihre Akkreditierungsanfrage. Unser Presseteam meldet sich zeitnah bei Ihnen.
+ {fallbackText(accreditation.successMessage, pressIndexContent.accreditation.successMessage)}
) : (
@@ -744,7 +715,7 @@ const Press: React.FC = () => {
marginBottom: 20,
}}
>
- Akkreditierung
+ {fallbackText(accreditation.eyebrow, pressIndexContent.accreditation.eyebrow)}
{
margin: '0 0 16px',
}}
>
- AKKREDITIERUNG ANFRAGEN.
+
{/* Gold divider */}
@@ -773,7 +744,7 @@ const Press: React.FC = () => {
marginBottom: 36,
}}
>
- Melden Sie sich für unsere Presse-Verteiler an oder fordern Sie eine Akkreditierung für die Gala-Verleihung an.
+ {fallbackText(accreditation.description, pressIndexContent.accreditation.description)}
{/* Form fields */}
@@ -789,7 +760,7 @@ const Press: React.FC = () => {
color: 'rgba(255,255,255,0.35)',
}}
>
- Medium / Redaktion *
+ {fallbackText(accreditation.mediumLabel, pressIndexContent.accreditation.mediumLabel)}
{
color: 'rgba(255,255,255,0.35)',
}}
>
- Ihr Name *
+ {fallbackText(accreditation.nameLabel, pressIndexContent.accreditation.nameLabel)}
{
color: 'rgba(255,255,255,0.35)',
}}
>
- E-Mail-Adresse *
+ {fallbackText(accreditation.emailLabel, pressIndexContent.accreditation.emailLabel)}
{
transition: 'background 0.15s, box-shadow 0.2s',
}}
>
- Akkreditierung anfragen
+ {fallbackText(accreditation.submitLabel, pressIndexContent.accreditation.submitLabel)}
>
diff --git a/src/spa/pressIndexContent.ts b/src/spa/pressIndexContent.ts
new file mode 100644
index 0000000..a06aa02
--- /dev/null
+++ b/src/spa/pressIndexContent.ts
@@ -0,0 +1,100 @@
+export const pressIndexContent = {
+ hero: {
+ imageFilename: 'gala-dinner.jpg',
+ imageAlt: 'Presse BMP',
+ eyebrow: 'Presse & Newsroom',
+ heading: 'EVENTS &\nBERICHTERSTATTUNG.',
+ description: 'Alle Termine, Pressemitteilungen und Medienmaterial rund um den Bayerischen Mittelstandspreis.',
+ },
+ events: {
+ eyebrow: 'Termine 2026',
+ heading: 'EVENTS RUND UM DEN PREIS.',
+ description: 'Von der Jurysitzung bis zur festlichen Gala – alle Veranstaltungen auf einen Blick.',
+ detailLabel: 'Details',
+ openStatusLabel: 'Anmeldung offen',
+ fallbackItems: [
+ {
+ title: 'Preisverleihung 2026',
+ date: '22. Okt 2026',
+ location: 'München, Residenz',
+ cat: 'Gala / Event',
+ status: 'In Planung',
+ img: '/images/buehne-moderatoren.jpg',
+ desc: 'Der glanzvolle Höhepunkt des Jahres. Die Verleihung der Preise in der prachtvollen Kulisse der Residenz München.',
+ slug: '/presse/events/preisverleihung-2026',
+ },
+ {
+ title: 'Mittelstands-Gipfel',
+ date: '15. Juni 2026',
+ location: 'Nürnberg',
+ cat: 'Workshop',
+ status: 'Anmeldung offen',
+ img: '/images/networking-innenhof.jpg',
+ desc: 'Regionaler Austausch und Best-Practices für bayerische Unternehmen auf dem Weg zum Preis.',
+ slug: '/presse/events/mittelstands-gipfel-2026',
+ },
+ {
+ title: 'Nominierten-Auswahl',
+ date: '04. Aug 2026',
+ location: 'Regensburg',
+ cat: 'Jury-Sitzung',
+ status: 'Intern',
+ img: '/images/saal-gedeckt.jpg',
+ desc: 'Das Gremium sichtet die Ergebnisse der Audits und legt die Nominierten für die Hauptpreise fest.',
+ slug: '/presse/events/nominierten-auswahl-2026',
+ },
+ ],
+ },
+ news: {
+ eyebrow: 'Newsroom',
+ heading: 'NACHRICHTEN & EINBLICKE.',
+ description: 'Berichte, Hintergründe und Einblicke rund um den bayerischen Mittelstand und den BMP.',
+ readMoreLabel: 'Weiterlesen',
+ fallbackItems: [
+ {
+ title: 'Wie Bayerns KMU die KI nutzen',
+ excerpt: 'Ein Deep-Dive in die Bewerbungsunterlagen 2024 zeigt: Der Mittelstand treibt die Digitalisierung aktiv voran.',
+ cat: 'Innovation',
+ img: '/images/gala-saal-overview.jpg',
+ slug: '/presse/blog/ki-nutzung-bayerischer-kmu',
+ },
+ {
+ title: 'Start der Ehrenamts-Initiative',
+ excerpt: 'Gemeinsam mit unseren Partnern fördern wir soziale Projekte mittelständischer Unternehmen.',
+ cat: 'Engagement',
+ img: '/images/networking-innenhof.jpg',
+ slug: '/presse/blog/ehrenamts-initiative-2026',
+ },
+ {
+ title: 'Bayerische Wirtschaft wächst',
+ excerpt: 'Neue Prognosen zeigen ein stabiles Wachstum für den Mittelstand – gute Aussichten für die Awards.',
+ cat: 'Wirtschaft',
+ img: '/images/gala-dinner.jpg',
+ slug: '/presse/blog/bayerische-wirtschaft-waechst',
+ },
+ ],
+ },
+ downloads: {
+ eyebrow: 'Pressekontakt & Material',
+ heading: 'PRESSE-MATERIAL & KONTAKT.',
+ description: 'Laden Sie unser offizielles Material herunter oder kontaktieren Sie direkt unser Presseteam für individuelle Anfragen.',
+ kitLabel: 'Download Presse-Kit',
+ kitMeta: 'Print_BMP.zip – 1,5 MB',
+ kitUrl: '/Print_BMP.zip',
+ kitDownloadName: 'Print_BMP.zip',
+ contactEyebrow: 'Pressekontakt',
+ contactName: 'Tanja Meier',
+ contactLines: 'presse@bmp-bayern.de\n+49 89 123 456 99',
+ },
+ accreditation: {
+ eyebrow: 'Akkreditierung',
+ heading: 'AKKREDITIERUNG ANFRAGEN.',
+ description: 'Melden Sie sich für unsere Presse-Verteiler an oder fordern Sie eine Akkreditierung für die Gala-Verleihung an.',
+ mediumLabel: 'Medium / Redaktion *',
+ nameLabel: 'Ihr Name *',
+ emailLabel: 'E-Mail-Adresse *',
+ submitLabel: 'Akkreditierung anfragen',
+ successHeading: 'Anfrage eingegangen',
+ successMessage: 'Vielen Dank für Ihre Akkreditierungsanfrage. Unser Presseteam meldet sich zeitnah bei Ihnen.',
+ },
+}