diff --git a/src/collections/Posts/index.ts b/src/collections/Posts/index.ts index 243a8b6..094e604 100644 --- a/src/collections/Posts/index.ts +++ b/src/collections/Posts/index.ts @@ -138,6 +138,15 @@ export const Posts: CollectionConfig<'posts'> = { 'Shared labels, category colors, meta copy, sidebar CTA, and related-article labels for the Presse detail route.', }, fields: [ + { + name: 'fallbackImage', + type: 'upload', + relationTo: 'media', + label: 'Fallback hero image', + admin: { + description: `Current frontend fallback image: /images/${blogDetailContent.fallbackImageFilename}`, + }, + }, { name: 'notFound', label: 'Not found state', diff --git a/src/payload-types.ts b/src/payload-types.ts index 927372b..88643c1 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -2139,6 +2139,10 @@ export interface Post { * Shared labels, category colors, meta copy, sidebar CTA, and related-article labels for the Presse detail route. */ detailPage?: { + /** + * Current frontend fallback image: /images/gala-saal-overview.jpg + */ + fallbackImage?: (number | null) | Media; notFound?: { title?: string | null; backLabel?: string | null; @@ -4998,6 +5002,7 @@ export interface PostsSelect { detailPage?: | T | { + fallbackImage?: T; notFound?: | T | { diff --git a/src/scripts/preload-blog-detail-cms.ts b/src/scripts/preload-blog-detail-cms.ts index 11fc748..044bbee 100644 --- a/src/scripts/preload-blog-detail-cms.ts +++ b/src/scripts/preload-blog-detail-cms.ts @@ -1,12 +1,24 @@ import 'dotenv/config' import config from '@payload-config' -import { getPayload } from 'payload' +import { getPayload, type Payload } from 'payload' import { blogDetailContent } from '@/spa/blogDetailContent' +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 fallbackImage = await mediaByFilename(payload, blogDetailContent.fallbackImageFilename) + const { fallbackImageFilename: _fallbackImageFilename, ...detailContent } = blogDetailContent const posts = await payload.find({ collection: 'posts', @@ -25,7 +37,8 @@ async function main() { context: { disableRevalidate: true }, data: { detailPage: { - ...blogDetailContent, + ...detailContent, + fallbackImage, metaCopy: blogDetailContent.meta, }, } as never, diff --git a/src/spa/blogDetailContent.ts b/src/spa/blogDetailContent.ts index f9962ec..dfec19a 100644 --- a/src/spa/blogDetailContent.ts +++ b/src/spa/blogDetailContent.ts @@ -1,4 +1,5 @@ export const blogDetailContent = { + fallbackImageFilename: 'gala-saal-overview.jpg', notFound: { title: 'Artikel nicht gefunden', backLabel: 'Zurück zur Presse', diff --git a/src/spa/pages/BlogDetail.tsx b/src/spa/pages/BlogDetail.tsx index 391ad24..336b1ec 100644 --- a/src/spa/pages/BlogDetail.tsx +++ b/src/spa/pages/BlogDetail.tsx @@ -4,7 +4,7 @@ import { ArrowLeft, Clock, User, Tag, ArrowRight } from 'lucide-react'; import { motion } from 'framer-motion'; import { getArticleBySlug, articles, type BlogArticle, type BlogSection } from '@/spa/data/blog'; import { useCmsRoute } from '@/spa/cmsRoute'; -import { docImageUrl } from '@/spa/cmsMediaField'; +import { docImageUrl, mediaUrl } from '@/spa/cmsMediaField'; import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { blogDetailContent } from '@/spa/blogDetailContent'; import Image from '@/spa/components/ui/UnoptimizedImage' @@ -17,6 +17,7 @@ const FF = '"IBM Plex Sans", sans-serif'; const FB = '"Inter", sans-serif'; type BlogDetailCms = Omit, 'categoryColors'> & { + fallbackImage?: unknown categoryColors?: unknown metaCopy?: Partial } @@ -46,6 +47,8 @@ export default function BlogDetail() { const { slug } = useParams<{ slug: string }>(); const route = useCmsRoute(); const cmsArticle = route?.collection === 'posts' ? route.doc : undefined; + const cmsDetail = getDetailPage(cmsArticle?.detailPage); + const articleFallbackImage = mediaUrl(cmsDetail.fallbackImage, `/images/${blogDetailContent.fallbackImageFilename}`); const article = (slug ? getArticleBySlug(slug) : undefined) || (cmsArticle ? { slug: String(cmsArticle.slug || slug), title: String(cmsArticle.title || 'Presse'), @@ -54,7 +57,7 @@ export default function BlogDetail() { author: String(cmsArticle.authorName || 'BMP Redaktion'), authorRole: String(cmsArticle.authorRole || 'Presse'), readTime: String(cmsArticle.readTime || '1 min'), - img: docImageUrl(cmsArticle, '/images/gala-saal-overview.jpg', 'heroImage'), + img: docImageUrl(cmsArticle, articleFallbackImage, 'heroImage'), excerpt: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.'), sections: Array.isArray(cmsArticle.sections) && cmsArticle.sections.length > 0 ? cmsArticle.sections as BlogSection[] : [{ body: String(cmsArticle.excerpt || cmsArticle.meta?.description || 'Dieser Pressebeitrag wird aus Payload CMS geladen.') }], tags: Array.isArray(cmsArticle.tags) @@ -69,7 +72,7 @@ export default function BlogDetail() { : [], detailPage: cmsArticle.detailPage, } as BlogArticle : undefined); - const detail = getDetailPage(article && 'detailPage' in article ? article.detailPage : undefined); + const detail = getDetailPage(article && 'detailPage' in article ? article.detailPage : cmsDetail); const notFound = detail.notFound || blogDetailContent.notFound; const heroBackLink = detail.heroBackLink || blogDetailContent.heroBackLink; const categoryColors = colorMap(detail.categoryColors);