feat: manage blog detail fallback media via payload

This commit is contained in:
syntaxbullet
2026-06-22 12:36:37 +02:00
parent a2c4df74e9
commit 79a9075f3a
5 changed files with 36 additions and 5 deletions

View File

@@ -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.', 'Shared labels, category colors, meta copy, sidebar CTA, and related-article labels for the Presse detail route.',
}, },
fields: [ fields: [
{
name: 'fallbackImage',
type: 'upload',
relationTo: 'media',
label: 'Fallback hero image',
admin: {
description: `Current frontend fallback image: /images/${blogDetailContent.fallbackImageFilename}`,
},
},
{ {
name: 'notFound', name: 'notFound',
label: 'Not found state', label: 'Not found state',

View File

@@ -2139,6 +2139,10 @@ export interface Post {
* Shared labels, category colors, meta copy, sidebar CTA, and related-article labels for the Presse detail route. * Shared labels, category colors, meta copy, sidebar CTA, and related-article labels for the Presse detail route.
*/ */
detailPage?: { detailPage?: {
/**
* Current frontend fallback image: /images/gala-saal-overview.jpg
*/
fallbackImage?: (number | null) | Media;
notFound?: { notFound?: {
title?: string | null; title?: string | null;
backLabel?: string | null; backLabel?: string | null;
@@ -4998,6 +5002,7 @@ export interface PostsSelect<T extends boolean = true> {
detailPage?: detailPage?:
| T | T
| { | {
fallbackImage?: T;
notFound?: notFound?:
| T | T
| { | {

View File

@@ -1,12 +1,24 @@
import 'dotenv/config' import 'dotenv/config'
import config from '@payload-config' import config from '@payload-config'
import { getPayload } from 'payload' import { getPayload, type Payload } from 'payload'
import { blogDetailContent } from '@/spa/blogDetailContent' 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() { async function main() {
const payload = await getPayload({ config }) const payload = await getPayload({ config })
const fallbackImage = await mediaByFilename(payload, blogDetailContent.fallbackImageFilename)
const { fallbackImageFilename: _fallbackImageFilename, ...detailContent } = blogDetailContent
const posts = await payload.find({ const posts = await payload.find({
collection: 'posts', collection: 'posts',
@@ -25,7 +37,8 @@ async function main() {
context: { disableRevalidate: true }, context: { disableRevalidate: true },
data: { data: {
detailPage: { detailPage: {
...blogDetailContent, ...detailContent,
fallbackImage,
metaCopy: blogDetailContent.meta, metaCopy: blogDetailContent.meta,
}, },
} as never, } as never,

View File

@@ -1,4 +1,5 @@
export const blogDetailContent = { export const blogDetailContent = {
fallbackImageFilename: 'gala-saal-overview.jpg',
notFound: { notFound: {
title: 'Artikel nicht gefunden', title: 'Artikel nicht gefunden',
backLabel: 'Zurück zur Presse', backLabel: 'Zurück zur Presse',

View File

@@ -4,7 +4,7 @@ import { ArrowLeft, Clock, User, Tag, ArrowRight } from 'lucide-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { getArticleBySlug, articles, type BlogArticle, type BlogSection } from '@/spa/data/blog'; import { getArticleBySlug, articles, type BlogArticle, type BlogSection } from '@/spa/data/blog';
import { useCmsRoute } from '@/spa/cmsRoute'; import { useCmsRoute } from '@/spa/cmsRoute';
import { docImageUrl } from '@/spa/cmsMediaField'; import { docImageUrl, mediaUrl } from '@/spa/cmsMediaField';
import { useIsMobile } from '@/spa/hooks/useIsMobile'; import { useIsMobile } from '@/spa/hooks/useIsMobile';
import { blogDetailContent } from '@/spa/blogDetailContent'; import { blogDetailContent } from '@/spa/blogDetailContent';
import Image from '@/spa/components/ui/UnoptimizedImage' import Image from '@/spa/components/ui/UnoptimizedImage'
@@ -17,6 +17,7 @@ const FF = '"IBM Plex Sans", sans-serif';
const FB = '"Inter", sans-serif'; const FB = '"Inter", sans-serif';
type BlogDetailCms = Omit<Partial<typeof blogDetailContent>, 'categoryColors'> & { type BlogDetailCms = Omit<Partial<typeof blogDetailContent>, 'categoryColors'> & {
fallbackImage?: unknown
categoryColors?: unknown categoryColors?: unknown
metaCopy?: Partial<typeof blogDetailContent.meta> metaCopy?: Partial<typeof blogDetailContent.meta>
} }
@@ -46,6 +47,8 @@ export default function BlogDetail() {
const { slug } = useParams<{ slug: string }>(); const { slug } = useParams<{ slug: string }>();
const route = useCmsRoute(); const route = useCmsRoute();
const cmsArticle = route?.collection === 'posts' ? route.doc : undefined; 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 ? { const article = (slug ? getArticleBySlug(slug) : undefined) || (cmsArticle ? {
slug: String(cmsArticle.slug || slug), slug: String(cmsArticle.slug || slug),
title: String(cmsArticle.title || 'Presse'), title: String(cmsArticle.title || 'Presse'),
@@ -54,7 +57,7 @@ export default function BlogDetail() {
author: String(cmsArticle.authorName || 'BMP Redaktion'), author: String(cmsArticle.authorName || 'BMP Redaktion'),
authorRole: String(cmsArticle.authorRole || 'Presse'), authorRole: String(cmsArticle.authorRole || 'Presse'),
readTime: String(cmsArticle.readTime || '1 min'), 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.'), 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.') }], 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) tags: Array.isArray(cmsArticle.tags)
@@ -69,7 +72,7 @@ export default function BlogDetail() {
: [], : [],
detailPage: cmsArticle.detailPage, detailPage: cmsArticle.detailPage,
} as BlogArticle : undefined); } 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 notFound = detail.notFound || blogDetailContent.notFound;
const heroBackLink = detail.heroBackLink || blogDetailContent.heroBackLink; const heroBackLink = detail.heroBackLink || blogDetailContent.heroBackLink;
const categoryColors = colorMap(detail.categoryColors); const categoryColors = colorMap(detail.categoryColors);