feat: manage blog detail chrome via payload
This commit is contained in:
@@ -17,6 +17,7 @@
|
|||||||
"migrate:spa-media": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/migrate-spa-media.ts",
|
"migrate:spa-media": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/migrate-spa-media.ts",
|
||||||
"migrate:spa-pages": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/migrate-spa-pages.ts",
|
"migrate:spa-pages": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/migrate-spa-pages.ts",
|
||||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||||
|
"preload:blog-detail-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-blog-detail-cms.ts",
|
||||||
"preload:about-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-about-page-cms.ts",
|
"preload:about-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-about-page-cms.ts",
|
||||||
"preload:contact-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-contact-page-cms.ts",
|
"preload:contact-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-contact-page-cms.ts",
|
||||||
"preload:datenschutz-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-datenschutz-page-cms.ts",
|
"preload:datenschutz-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-datenschutz-page-cms.ts",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
|
|||||||
import { Banner } from '../../blocks/Banner/config'
|
import { Banner } from '../../blocks/Banner/config'
|
||||||
import { Code } from '../../blocks/Code/config'
|
import { Code } from '../../blocks/Code/config'
|
||||||
import { MediaBlock } from '../../blocks/MediaBlock/config'
|
import { MediaBlock } from '../../blocks/MediaBlock/config'
|
||||||
|
import { blogDetailContent } from '@/spa/blogDetailContent'
|
||||||
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
|
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
|
||||||
import { populateAuthors } from './hooks/populateAuthors'
|
import { populateAuthors } from './hooks/populateAuthors'
|
||||||
import { revalidateDelete, revalidatePost } from './hooks/revalidatePost'
|
import { revalidateDelete, revalidatePost } from './hooks/revalidatePost'
|
||||||
@@ -128,6 +129,68 @@ export const Posts: CollectionConfig<'posts'> = {
|
|||||||
type: 'array',
|
type: 'array',
|
||||||
fields: [{ name: 'slug', type: 'text', required: true }],
|
fields: [{ name: 'slug', type: 'text', required: true }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'detailPage',
|
||||||
|
label: 'Detail page chrome',
|
||||||
|
type: 'group',
|
||||||
|
admin: {
|
||||||
|
description:
|
||||||
|
'Shared labels, category colors, meta copy, sidebar CTA, and related-article labels for the Presse detail route.',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'notFound',
|
||||||
|
label: 'Not found state',
|
||||||
|
type: 'group',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text', defaultValue: blogDetailContent.notFound.title },
|
||||||
|
{ name: 'backLabel', type: 'text', defaultValue: blogDetailContent.notFound.backLabel },
|
||||||
|
{ name: 'backUrl', type: 'text', defaultValue: blogDetailContent.notFound.backUrl },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'heroBackLink',
|
||||||
|
label: 'Hero back link',
|
||||||
|
type: 'group',
|
||||||
|
fields: [
|
||||||
|
{ name: 'label', type: 'text', defaultValue: blogDetailContent.heroBackLink.label },
|
||||||
|
{ name: 'url', type: 'text', defaultValue: blogDetailContent.heroBackLink.url },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'categoryColors',
|
||||||
|
label: 'Category colors JSON',
|
||||||
|
type: 'json',
|
||||||
|
defaultValue: blogDetailContent.categoryColors,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'metaCopy',
|
||||||
|
label: 'Meta copy',
|
||||||
|
type: 'group',
|
||||||
|
fields: [{ name: 'readTimeSuffix', type: 'text', defaultValue: blogDetailContent.meta.readTimeSuffix }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sidebarCta',
|
||||||
|
label: 'Sidebar CTA',
|
||||||
|
type: 'group',
|
||||||
|
fields: [
|
||||||
|
{ name: 'heading', type: 'text', defaultValue: blogDetailContent.sidebarCta.heading },
|
||||||
|
{ name: 'text', type: 'textarea', defaultValue: blogDetailContent.sidebarCta.text },
|
||||||
|
{ name: 'label', type: 'text', defaultValue: blogDetailContent.sidebarCta.label },
|
||||||
|
{ name: 'url', type: 'text', defaultValue: blogDetailContent.sidebarCta.url },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'related',
|
||||||
|
label: 'Related articles',
|
||||||
|
type: 'group',
|
||||||
|
fields: [
|
||||||
|
{ name: 'heading', type: 'text', defaultValue: blogDetailContent.related.heading },
|
||||||
|
{ name: 'readMoreLabel', type: 'text', defaultValue: blogDetailContent.related.readMoreLabel },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'content',
|
name: 'content',
|
||||||
type: 'richText',
|
type: 'richText',
|
||||||
|
|||||||
@@ -1879,6 +1879,42 @@ export interface Post {
|
|||||||
id?: string | null;
|
id?: string | null;
|
||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
|
/**
|
||||||
|
* Shared labels, category colors, meta copy, sidebar CTA, and related-article labels for the Presse detail route.
|
||||||
|
*/
|
||||||
|
detailPage?: {
|
||||||
|
notFound?: {
|
||||||
|
title?: string | null;
|
||||||
|
backLabel?: string | null;
|
||||||
|
backUrl?: string | null;
|
||||||
|
};
|
||||||
|
heroBackLink?: {
|
||||||
|
label?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
categoryColors?:
|
||||||
|
| {
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
| unknown[]
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null;
|
||||||
|
metaCopy?: {
|
||||||
|
readTimeSuffix?: string | null;
|
||||||
|
};
|
||||||
|
sidebarCta?: {
|
||||||
|
heading?: string | null;
|
||||||
|
text?: string | null;
|
||||||
|
label?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
related?: {
|
||||||
|
heading?: string | null;
|
||||||
|
readMoreLabel?: string | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
content: {
|
content: {
|
||||||
root: {
|
root: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -4465,6 +4501,43 @@ export interface PostsSelect<T extends boolean = true> {
|
|||||||
slug?: T;
|
slug?: T;
|
||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
|
detailPage?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
notFound?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
backLabel?: T;
|
||||||
|
backUrl?: T;
|
||||||
|
};
|
||||||
|
heroBackLink?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
label?: T;
|
||||||
|
url?: T;
|
||||||
|
};
|
||||||
|
categoryColors?: T;
|
||||||
|
metaCopy?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
readTimeSuffix?: T;
|
||||||
|
};
|
||||||
|
sidebarCta?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
heading?: T;
|
||||||
|
text?: T;
|
||||||
|
label?: T;
|
||||||
|
url?: T;
|
||||||
|
};
|
||||||
|
related?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
heading?: T;
|
||||||
|
readMoreLabel?: T;
|
||||||
|
};
|
||||||
|
};
|
||||||
content?: T;
|
content?: T;
|
||||||
relatedPosts?: T;
|
relatedPosts?: T;
|
||||||
categories?: T;
|
categories?: T;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'dotenv/config'
|
|||||||
import { getPayload } from 'payload'
|
import { getPayload } from 'payload'
|
||||||
|
|
||||||
import { articles } from '@/spa/data/blog'
|
import { articles } from '@/spa/data/blog'
|
||||||
|
import { blogDetailContent } from '@/spa/blogDetailContent'
|
||||||
import { eventDetailContent } from '@/spa/eventDetailContent'
|
import { eventDetailContent } from '@/spa/eventDetailContent'
|
||||||
import { events } from '@/spa/data/events'
|
import { events } from '@/spa/data/events'
|
||||||
import { preistraegerDetailContent } from '@/spa/preistraegerDetailContent'
|
import { preistraegerDetailContent } from '@/spa/preistraegerDetailContent'
|
||||||
@@ -130,6 +131,10 @@ function postData(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
|
|||||||
sections: article?.sections || [],
|
sections: article?.sections || [],
|
||||||
tags: article?.tags?.map((tag) => ({ tag })) || [],
|
tags: article?.tags?.map((tag) => ({ tag })) || [],
|
||||||
relatedSlugs: article?.relatedSlugs?.map((slug) => ({ slug })) || [],
|
relatedSlugs: article?.relatedSlugs?.map((slug) => ({ slug })) || [],
|
||||||
|
detailPage: {
|
||||||
|
...blogDetailContent,
|
||||||
|
metaCopy: blogDetailContent.meta,
|
||||||
|
},
|
||||||
content: emptyRichText(
|
content: emptyRichText(
|
||||||
article?.sections?.map((section) => [section.heading, section.body].filter(Boolean).join('\n')).join('\n\n') ||
|
article?.sections?.map((section) => [section.heading, section.body].filter(Boolean).join('\n')).join('\n\n') ||
|
||||||
`Dieser Presseartikel wird aktuell noch von der migrierten SPA gerendert. Über „Preview“ kann der aktuelle Stand unter ${page.path} betrachtet werden.`,
|
`Dieser Presseartikel wird aktuell noch von der migrierten SPA gerendert. Über „Preview“ kann der aktuelle Stand unter ${page.path} betrachtet werden.`,
|
||||||
|
|||||||
42
src/scripts/preload-blog-detail-cms.ts
Normal file
42
src/scripts/preload-blog-detail-cms.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import 'dotenv/config'
|
||||||
|
|
||||||
|
import config from '@payload-config'
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
|
||||||
|
import { blogDetailContent } from '@/spa/blogDetailContent'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const payload = await getPayload({ config })
|
||||||
|
|
||||||
|
const posts = await payload.find({
|
||||||
|
collection: 'posts',
|
||||||
|
draft: true,
|
||||||
|
overrideAccess: true,
|
||||||
|
pagination: false,
|
||||||
|
limit: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
posts.docs.map((post) =>
|
||||||
|
payload.update({
|
||||||
|
collection: 'posts',
|
||||||
|
id: post.id,
|
||||||
|
overrideAccess: true,
|
||||||
|
context: { disableRevalidate: true },
|
||||||
|
data: {
|
||||||
|
detailPage: {
|
||||||
|
...blogDetailContent,
|
||||||
|
metaCopy: blogDetailContent.meta,
|
||||||
|
},
|
||||||
|
} as never,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
payload.logger.info(`Preloaded Blog detail CMS fields for ${posts.docs.length} records`)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(error)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
29
src/spa/blogDetailContent.ts
Normal file
29
src/spa/blogDetailContent.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export const blogDetailContent = {
|
||||||
|
notFound: {
|
||||||
|
title: 'Artikel nicht gefunden',
|
||||||
|
backLabel: 'Zurück zur Presse',
|
||||||
|
backUrl: '/presse',
|
||||||
|
},
|
||||||
|
heroBackLink: {
|
||||||
|
label: 'Newsroom',
|
||||||
|
url: '/presse',
|
||||||
|
},
|
||||||
|
categoryColors: {
|
||||||
|
Innovation: '#2563EB',
|
||||||
|
Engagement: '#16A34A',
|
||||||
|
Wirtschaft: '#9333EA',
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
readTimeSuffix: 'Lesezeit',
|
||||||
|
},
|
||||||
|
sidebarCta: {
|
||||||
|
heading: 'Jetzt bewerben',
|
||||||
|
text: 'Ist Ihr Unternehmen bereit für den Bayerischen Mittelstandspreis 2026?',
|
||||||
|
label: 'Zur Bewerbung',
|
||||||
|
url: '/teilnahme',
|
||||||
|
},
|
||||||
|
related: {
|
||||||
|
heading: 'Weitere Artikel',
|
||||||
|
readMoreLabel: 'Weiterlesen',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { getArticleBySlug, articles, type BlogArticle, type BlogSection } from '
|
|||||||
import { useCmsRoute } from '@/spa/cmsRoute';
|
import { useCmsRoute } from '@/spa/cmsRoute';
|
||||||
import { docImageUrl } from '@/spa/cmsMediaField';
|
import { docImageUrl } from '@/spa/cmsMediaField';
|
||||||
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
import { useIsMobile } from '@/spa/hooks/useIsMobile';
|
||||||
|
import { blogDetailContent } from '@/spa/blogDetailContent';
|
||||||
import Image from '@/spa/components/ui/UnoptimizedImage'
|
import Image from '@/spa/components/ui/UnoptimizedImage'
|
||||||
|
|
||||||
const NAVY = '#111D55';
|
const NAVY = '#111D55';
|
||||||
@@ -15,10 +16,29 @@ const INK = '#3A3A3A';
|
|||||||
const FF = '"IBM Plex Sans", sans-serif';
|
const FF = '"IBM Plex Sans", sans-serif';
|
||||||
const FB = '"Inter", sans-serif';
|
const FB = '"Inter", sans-serif';
|
||||||
|
|
||||||
const CAT_COLORS: Record<string, string> = {
|
type BlogDetailCms = Omit<Partial<typeof blogDetailContent>, 'categoryColors'> & {
|
||||||
Innovation: '#2563EB',
|
categoryColors?: unknown
|
||||||
Engagement: '#16A34A',
|
metaCopy?: Partial<typeof blogDetailContent.meta>
|
||||||
Wirtschaft: '#9333EA',
|
}
|
||||||
|
|
||||||
|
const fallbackText = (value: unknown, fallback: string) => typeof value === 'string' && value.length > 0 ? value : fallback;
|
||||||
|
const colorMap = (value: unknown): Record<string, string> => {
|
||||||
|
if (!value || typeof value !== 'object') return blogDetailContent.categoryColors;
|
||||||
|
return { ...blogDetailContent.categoryColors, ...(value as Record<string, string>) };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDetailPage = (value: unknown): BlogDetailCms => {
|
||||||
|
const detail = (value && typeof value === 'object' ? value : {}) as BlogDetailCms;
|
||||||
|
return {
|
||||||
|
...blogDetailContent,
|
||||||
|
...detail,
|
||||||
|
notFound: { ...blogDetailContent.notFound, ...detail.notFound },
|
||||||
|
heroBackLink: { ...blogDetailContent.heroBackLink, ...detail.heroBackLink },
|
||||||
|
categoryColors: colorMap(detail.categoryColors),
|
||||||
|
metaCopy: { ...blogDetailContent.meta, ...(detail.metaCopy || detail.meta) },
|
||||||
|
sidebarCta: { ...blogDetailContent.sidebarCta, ...detail.sidebarCta },
|
||||||
|
related: { ...blogDetailContent.related, ...detail.related },
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function BlogDetail() {
|
export default function BlogDetail() {
|
||||||
@@ -47,14 +67,22 @@ export default function BlogDetail() {
|
|||||||
.map((item: { slug?: string }) => item.slug)
|
.map((item: { slug?: string }) => item.slug)
|
||||||
.filter((slug): slug is string => Boolean(slug))
|
.filter((slug): slug is string => Boolean(slug))
|
||||||
: [],
|
: [],
|
||||||
|
detailPage: cmsArticle.detailPage,
|
||||||
} as BlogArticle : undefined);
|
} as BlogArticle : undefined);
|
||||||
|
const detail = getDetailPage(article && 'detailPage' in article ? article.detailPage : undefined);
|
||||||
|
const notFound = detail.notFound || blogDetailContent.notFound;
|
||||||
|
const heroBackLink = detail.heroBackLink || blogDetailContent.heroBackLink;
|
||||||
|
const categoryColors = colorMap(detail.categoryColors);
|
||||||
|
const metaCopy = detail.metaCopy || blogDetailContent.meta;
|
||||||
|
const sidebarCta = detail.sidebarCta || blogDetailContent.sidebarCta;
|
||||||
|
const relatedCopy = detail.related || blogDetailContent.related;
|
||||||
|
|
||||||
if (!article) {
|
if (!article) {
|
||||||
return (
|
return (
|
||||||
<div style={{ minHeight: '60vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 24, padding: '80px 24px', background: CREAM }}>
|
<div style={{ minHeight: '60vh', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 24, padding: '80px 24px', background: CREAM }}>
|
||||||
<h2 style={{ fontFamily: FF, fontSize: '2rem', fontWeight: 900, color: NAVY }}>Artikel nicht gefunden</h2>
|
<h2 style={{ fontFamily: FF, fontSize: '2rem', fontWeight: 900, color: NAVY }}>{fallbackText(notFound.title, blogDetailContent.notFound.title)}</h2>
|
||||||
<Link to="/presse" style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: NAVY, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6 }}>
|
<Link to={fallbackText(notFound.backUrl, blogDetailContent.notFound.backUrl)} style={{ fontFamily: FF, fontSize: 16, fontWeight: 700, color: NAVY, textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||||
<ArrowLeft size={14} /> Zurück zur Presse
|
<ArrowLeft size={14} /> {fallbackText(notFound.backLabel, blogDetailContent.notFound.backLabel)}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -82,7 +110,7 @@ export default function BlogDetail() {
|
|||||||
<div style={{ position: 'relative', zIndex: 2, padding: isMobile ? '0 24px 48px' : '0 80px 72px', maxWidth: 860 }}>
|
<div style={{ position: 'relative', zIndex: 2, padding: isMobile ? '0 24px 48px' : '0 80px 72px', maxWidth: 860 }}>
|
||||||
{/* Back link */}
|
{/* Back link */}
|
||||||
<Link
|
<Link
|
||||||
to="/presse"
|
to={fallbackText(heroBackLink.url, blogDetailContent.heroBackLink.url)}
|
||||||
style={{
|
style={{
|
||||||
display: 'inline-flex', alignItems: 'center', gap: 8,
|
display: 'inline-flex', alignItems: 'center', gap: 8,
|
||||||
fontFamily: FF, fontSize: 11, fontWeight: 700,
|
fontFamily: FF, fontSize: 11, fontWeight: 700,
|
||||||
@@ -93,7 +121,7 @@ export default function BlogDetail() {
|
|||||||
onMouseEnter={e => (e.currentTarget.style.color = '#fff')}
|
onMouseEnter={e => (e.currentTarget.style.color = '#fff')}
|
||||||
onMouseLeave={e => (e.currentTarget.style.color = 'rgba(255,255,255,0.5)')}
|
onMouseLeave={e => (e.currentTarget.style.color = 'rgba(255,255,255,0.5)')}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={13} /> Newsroom
|
<ArrowLeft size={13} /> {fallbackText(heroBackLink.label, blogDetailContent.heroBackLink.label)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Category badge */}
|
{/* Category badge */}
|
||||||
@@ -103,7 +131,7 @@ export default function BlogDetail() {
|
|||||||
padding: '4px 12px',
|
padding: '4px 12px',
|
||||||
fontFamily: FF, fontSize: 9, fontWeight: 700,
|
fontFamily: FF, fontSize: 9, fontWeight: 700,
|
||||||
textTransform: 'uppercase', letterSpacing: '0.22em',
|
textTransform: 'uppercase', letterSpacing: '0.22em',
|
||||||
background: CAT_COLORS[article.cat] ?? GOLD,
|
background: categoryColors[article.cat] ?? GOLD,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
}}>
|
}}>
|
||||||
{article.cat}
|
{article.cat}
|
||||||
@@ -133,7 +161,7 @@ export default function BlogDetail() {
|
|||||||
<div style={{ display: 'flex', gap: 28, flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: 28, flexWrap: 'wrap' }}>
|
||||||
{[
|
{[
|
||||||
{ icon: User, label: `${article.author} · ${article.authorRole}` },
|
{ icon: User, label: `${article.author} · ${article.authorRole}` },
|
||||||
{ icon: Clock, label: `${article.date} · ${article.readTime} Lesezeit` },
|
{ icon: Clock, label: `${article.date} · ${article.readTime} ${fallbackText(metaCopy.readTimeSuffix, blogDetailContent.meta.readTimeSuffix)}` },
|
||||||
].map(({ icon: Icon, label }, i) => (
|
].map(({ icon: Icon, label }, i) => (
|
||||||
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, fontFamily: FB, fontSize: 16, color: 'rgba(255,255,255,0.5)' }}>
|
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 8, fontFamily: FB, fontSize: 16, color: 'rgba(255,255,255,0.5)' }}>
|
||||||
<Icon size={13} color={GOLD} /> {label}
|
<Icon size={13} color={GOLD} /> {label}
|
||||||
@@ -225,13 +253,13 @@ export default function BlogDetail() {
|
|||||||
<div style={{ background: '#fff', border: '1px solid rgba(3,9,58,0.1)', padding: '28px' }}>
|
<div style={{ background: '#fff', border: '1px solid rgba(3,9,58,0.1)', padding: '28px' }}>
|
||||||
<div style={{ width: 32, height: 2, background: GOLD, marginBottom: 16 }} />
|
<div style={{ width: 32, height: 2, background: GOLD, marginBottom: 16 }} />
|
||||||
<h4 style={{ fontFamily: FF, fontSize: 15, fontWeight: 900, color: NAVY, textTransform: 'uppercase', letterSpacing: '-0.01em', marginBottom: 12 }}>
|
<h4 style={{ fontFamily: FF, fontSize: 15, fontWeight: 900, color: NAVY, textTransform: 'uppercase', letterSpacing: '-0.01em', marginBottom: 12 }}>
|
||||||
Jetzt bewerben
|
{fallbackText(sidebarCta.heading, blogDetailContent.sidebarCta.heading)}
|
||||||
</h4>
|
</h4>
|
||||||
<p style={{ fontFamily: FB, fontSize: 16, color: 'rgba(58,58,58,0.55)', lineHeight: 1.7, marginBottom: 20 }}>
|
<p style={{ fontFamily: FB, fontSize: 16, color: 'rgba(58,58,58,0.55)', lineHeight: 1.7, marginBottom: 20 }}>
|
||||||
Ist Ihr Unternehmen bereit für den Bayerischen Mittelstandspreis 2026?
|
{fallbackText(sidebarCta.text, blogDetailContent.sidebarCta.text)}
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
to="/teilnahme"
|
to={fallbackText(sidebarCta.url, blogDetailContent.sidebarCta.url)}
|
||||||
style={{
|
style={{
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
|
||||||
padding: '13px 20px',
|
padding: '13px 20px',
|
||||||
@@ -244,7 +272,7 @@ export default function BlogDetail() {
|
|||||||
onMouseEnter={e => (e.currentTarget.style.background = GOLD)}
|
onMouseEnter={e => (e.currentTarget.style.background = GOLD)}
|
||||||
onMouseLeave={e => (e.currentTarget.style.background = NAVY)}
|
onMouseLeave={e => (e.currentTarget.style.background = NAVY)}
|
||||||
>
|
>
|
||||||
Zur Bewerbung <ArrowRight size={12} />
|
{fallbackText(sidebarCta.label, blogDetailContent.sidebarCta.label)} <ArrowRight size={12} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -259,14 +287,14 @@ export default function BlogDetail() {
|
|||||||
|
|
||||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 20, marginBottom: 56 }}>
|
<div style={{ display: 'flex', alignItems: 'baseline', gap: 20, marginBottom: 56 }}>
|
||||||
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.28em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.35)' }}>
|
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, letterSpacing: '0.28em', textTransform: 'uppercase', color: 'rgba(255,255,255,0.35)' }}>
|
||||||
Weitere Artikel
|
{fallbackText(relatedCopy.heading, blogDetailContent.related.heading)}
|
||||||
</span>
|
</span>
|
||||||
<div style={{ flex: 1, height: 1, background: 'rgba(255,255,255,0.1)' }} />
|
<div style={{ flex: 1, height: 1, background: 'rgba(255,255,255,0.1)' }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : `repeat(${related.length}, 1fr)`, gap: 0 }}>
|
<div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : `repeat(${related.length}, 1fr)`, gap: 0 }}>
|
||||||
{related.map((rel, idx) => (
|
{related.map((rel, idx) => (
|
||||||
<RelatedCard key={rel.slug} article={rel} bordered={isMobile ? false : idx > 0} isMobile={isMobile} />
|
<RelatedCard key={rel.slug} article={rel} bordered={isMobile ? false : idx > 0} isMobile={isMobile} readMoreLabel={fallbackText(relatedCopy.readMoreLabel, blogDetailContent.related.readMoreLabel)} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -277,7 +305,7 @@ export default function BlogDetail() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RelatedCard({ article, bordered, isMobile }: { article: ReturnType<typeof getArticleBySlug> & object; bordered: boolean; isMobile: boolean }) {
|
function RelatedCard({ article, bordered, isMobile, readMoreLabel }: { article: ReturnType<typeof getArticleBySlug> & object; bordered: boolean; isMobile: boolean; readMoreLabel: string }) {
|
||||||
const [hovered, setHovered] = React.useState(false);
|
const [hovered, setHovered] = React.useState(false);
|
||||||
if (!article) return null;
|
if (!article) return null;
|
||||||
return (
|
return (
|
||||||
@@ -319,7 +347,7 @@ function RelatedCard({ article, bordered, isMobile }: { article: ReturnType<type
|
|||||||
{article.excerpt}
|
{article.excerpt}
|
||||||
</p>
|
</p>
|
||||||
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: GOLD, display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
<span style={{ fontFamily: FF, fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.12em', color: GOLD, display: 'inline-flex', alignItems: 'center', gap: 5 }}>
|
||||||
Weiterlesen <ArrowRight size={12} />
|
{readMoreLabel} <ArrowRight size={12} />
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user