feat: Add fresh database setup script and media handling improvements
- Introduced a new script for setting up a fresh database with ordered migration tasks. - Added a utility function to find media by filename, improving media handling in various scripts. - Updated preload scripts to utilize the new media fetching utility. - Refactored datenschutz, impressum, and mitglied-werden fields to use stable IDs and anchor IDs. - Enhanced home and network pages to accommodate new field structures. - Added new preload script for global CMS settings. - Improved code readability and organization across multiple files.
This commit is contained in:
@@ -17,7 +17,9 @@
|
||||
"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",
|
||||
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
|
||||
"setup:fresh-db": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/setup-fresh-database.ts",
|
||||
"preload:blog-detail-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-blog-detail-cms.ts",
|
||||
"preload:globals-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-globals-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:datenschutz-page-cms": "cross-env NODE_OPTIONS=--no-deprecation tsx src/scripts/preload-datenschutz-page-cms.ts",
|
||||
|
||||
@@ -21,6 +21,11 @@ const sectionAdmin = (description: string) => ({
|
||||
initCollapsed: true,
|
||||
})
|
||||
|
||||
const sectionsDefault = datenschutzContent.sections.map(({ id, ...section }) => ({
|
||||
...section,
|
||||
anchorId: id,
|
||||
}))
|
||||
|
||||
export const datenschutzFields: Field[] = [
|
||||
{
|
||||
name: 'datenschutz',
|
||||
@@ -60,9 +65,9 @@ export const datenschutzFields: Field[] = [
|
||||
type: 'array',
|
||||
dbName: 'data_sections',
|
||||
admin: sectionAdmin('Each section controls one heading, table-of-contents label, and ordered JSON body blocks.'),
|
||||
defaultValue: datenschutzContent.sections,
|
||||
defaultValue: sectionsDefault,
|
||||
fields: [
|
||||
text('id', 'Anchor ID'),
|
||||
text('anchorId', 'Anchor ID'),
|
||||
text('tocLabel', 'TOC label'),
|
||||
text('title', 'Section title'),
|
||||
{
|
||||
|
||||
@@ -180,8 +180,20 @@ export const homeFields: Field[] = [
|
||||
maxRows: 3,
|
||||
defaultValue: homeContent.status.phases,
|
||||
fields: [
|
||||
text('num', 'Slide number'), text('tag', 'Badge'), { name: 'pulse', label: 'Show pulse dot', type: 'checkbox', defaultValue: true }, text('phase', 'Phase label'), text('headline', 'Headline'), textarea('body', 'Body copy'),
|
||||
{ name: 'cta', label: 'Slide CTA', type: 'group', fields: linkGroup('', '') }, text('accent', 'Accent color'), text('bg', 'Background color'), text('glow', 'Glow color'), text('meta', 'Meta label'), text('successApplications', 'Success applications number'), text('successWinners', 'Success winners number'), { name: 'membershipCta', label: 'Membership CTA', type: 'group', fields: linkGroup('Mitglied werden', '/mitglied-werden') },
|
||||
text('num', 'Slide number'),
|
||||
text('tag', 'Badge'),
|
||||
{ name: 'pulse', label: 'Show pulse dot', type: 'checkbox', defaultValue: true },
|
||||
text('phase', 'Phase label'),
|
||||
text('headline', 'Headline'),
|
||||
textarea('body', 'Body copy'),
|
||||
{ name: 'cta', label: 'Slide CTA', type: 'group', fields: linkGroup('', '') },
|
||||
text('accent', 'Accent color'),
|
||||
text('bg', 'Background color'),
|
||||
text('glow', 'Glow color'),
|
||||
text('meta', 'Meta label'),
|
||||
text('successApplications', 'Success applications number'),
|
||||
text('successWinners', 'Success winners number'),
|
||||
{ name: 'membershipCta', label: 'Membership CTA', type: 'group', fields: linkGroup('Mitglied werden', '/mitglied-werden') },
|
||||
],
|
||||
},
|
||||
text('noActionLabel', 'No CTA fallback label', homeContent.status.noActionLabel),
|
||||
|
||||
@@ -21,6 +21,11 @@ const sectionAdmin = (description: string) => ({
|
||||
initCollapsed: true,
|
||||
})
|
||||
|
||||
const sectionsDefault = impressumContent.sections.map(({ id, ...section }) => ({
|
||||
...section,
|
||||
anchorId: id,
|
||||
}))
|
||||
|
||||
export const impressumFields: Field[] = [
|
||||
{
|
||||
name: 'impressum',
|
||||
@@ -60,9 +65,9 @@ export const impressumFields: Field[] = [
|
||||
type: 'array',
|
||||
dbName: 'imp_sections',
|
||||
admin: sectionAdmin('Each section controls one heading, table-of-contents label, and ordered JSON body blocks.'),
|
||||
defaultValue: impressumContent.sections,
|
||||
defaultValue: sectionsDefault,
|
||||
fields: [
|
||||
text('id', 'Anchor ID'),
|
||||
text('anchorId', 'Anchor ID'),
|
||||
text('tocLabel', 'TOC label'),
|
||||
text('title', 'Section title'),
|
||||
{
|
||||
|
||||
@@ -46,6 +46,10 @@ const sectionAdmin = (description: string) => ({
|
||||
const statFields: Field[] = [text('value', 'Value'), text('label', 'Label')]
|
||||
const factFields: Field[] = [text('num', 'Number'), text('label', 'Label'), textarea('body', 'Body')]
|
||||
const faqFields: Field[] = [text('q', 'Question'), textarea('a', 'Answer')]
|
||||
const applicationOptionDefaults = mitgliedWerdenContent.applicationOptions.items.map(({ id, ...item }) => ({
|
||||
...item,
|
||||
stableId: id,
|
||||
}))
|
||||
const testimonialFields: Field[] = [
|
||||
textarea('quote', 'Quote'),
|
||||
text('initials', 'Initials'),
|
||||
@@ -204,9 +208,9 @@ export const mitgliedWerdenFields: Field[] = [
|
||||
label: 'Options',
|
||||
type: 'array',
|
||||
dbName: 'member_app_options',
|
||||
defaultValue: mitgliedWerdenContent.applicationOptions.items,
|
||||
defaultValue: applicationOptionDefaults,
|
||||
fields: [
|
||||
text('id', 'Stable ID'),
|
||||
text('stableId', 'Stable ID'),
|
||||
text('number', 'Number'),
|
||||
text('eyebrow', 'Eyebrow'),
|
||||
text('title', 'Title'),
|
||||
|
||||
@@ -59,7 +59,7 @@ const juryMemberFields: Field[] = [
|
||||
const rowFields: Field[] = [text('num', 'Number'), text('label', 'Label'), textarea('body', 'Body')]
|
||||
|
||||
const partnerFields: Field[] = [
|
||||
text('id', 'Stable ID'),
|
||||
text('stableId', 'Stable ID'),
|
||||
text('name', 'Name'),
|
||||
text('role', 'Role'),
|
||||
numberField('tier', 'Tier'),
|
||||
@@ -94,6 +94,10 @@ const partnerFields: Field[] = [
|
||||
const benefitFields: Field[] = [text('num', 'Number'), text('label', 'Label'), text('sub', 'Subline')]
|
||||
const packageFields: Field[] = [text('value', 'Value'), text('title', 'Title'), textarea('desc', 'Description')]
|
||||
const formStepFields: Field[] = [text('label', 'Label')]
|
||||
const partnerDefaults = netzwerkContent.partners.items.map(({ id, ...partner }) => ({
|
||||
...partner,
|
||||
stableId: id,
|
||||
}))
|
||||
|
||||
export const netzwerkFields: Field[] = [
|
||||
{
|
||||
@@ -252,7 +256,7 @@ export const netzwerkFields: Field[] = [
|
||||
label: 'Partners',
|
||||
type: 'array',
|
||||
dbName: 'network_partners',
|
||||
defaultValue: netzwerkContent.partners.items,
|
||||
defaultValue: partnerDefaults,
|
||||
fields: partnerFields,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1358,7 +1358,7 @@ export interface Page {
|
||||
*/
|
||||
sections?:
|
||||
| {
|
||||
id?: string | null;
|
||||
anchorId?: string | null;
|
||||
tocLabel?: string | null;
|
||||
title?: string | null;
|
||||
/**
|
||||
@@ -1373,6 +1373,7 @@ export interface Page {
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
@@ -1402,7 +1403,7 @@ export interface Page {
|
||||
*/
|
||||
sections?:
|
||||
| {
|
||||
id?: string | null;
|
||||
anchorId?: string | null;
|
||||
tocLabel?: string | null;
|
||||
title?: string | null;
|
||||
/**
|
||||
@@ -1417,6 +1418,7 @@ export interface Page {
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
@@ -1823,7 +1825,7 @@ export interface Page {
|
||||
};
|
||||
items?:
|
||||
| {
|
||||
id?: string | null;
|
||||
stableId?: string | null;
|
||||
name?: string | null;
|
||||
role?: string | null;
|
||||
tier?: number | null;
|
||||
@@ -1838,6 +1840,7 @@ export interface Page {
|
||||
website?: string | null;
|
||||
linkedin?: string | null;
|
||||
heroImg?: string | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
@@ -2022,7 +2025,7 @@ export interface Page {
|
||||
applicationOptions?: {
|
||||
items?:
|
||||
| {
|
||||
id?: string | null;
|
||||
stableId?: string | null;
|
||||
number?: string | null;
|
||||
eyebrow?: string | null;
|
||||
title?: string | null;
|
||||
@@ -2030,6 +2033,7 @@ export interface Page {
|
||||
href?: string | null;
|
||||
file?: (number | null) | Media;
|
||||
download?: boolean | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
};
|
||||
@@ -4304,10 +4308,11 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
sections?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
anchorId?: T;
|
||||
tocLabel?: T;
|
||||
title?: T;
|
||||
items?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
datenschutz?:
|
||||
@@ -4331,10 +4336,11 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
sections?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
anchorId?: T;
|
||||
tocLabel?: T;
|
||||
title?: T;
|
||||
items?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
preistraegerIndex?:
|
||||
@@ -4703,7 +4709,7 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
stableId?: T;
|
||||
name?: T;
|
||||
role?: T;
|
||||
tier?: T;
|
||||
@@ -4718,6 +4724,7 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
website?: T;
|
||||
linkedin?: T;
|
||||
heroImg?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
sponsoring?:
|
||||
@@ -4887,7 +4894,7 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
id?: T;
|
||||
stableId?: T;
|
||||
number?: T;
|
||||
eyebrow?: T;
|
||||
title?: T;
|
||||
@@ -4895,6 +4902,7 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
href?: T;
|
||||
file?: T;
|
||||
download?: T;
|
||||
id?: T;
|
||||
};
|
||||
};
|
||||
formIntro?:
|
||||
|
||||
35
src/scripts/mediaByFilename.ts
Normal file
35
src/scripts/mediaByFilename.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import path from 'path'
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
|
||||
export async function findMediaByFilename(payload: Payload, filename: string) {
|
||||
const exact = await payload.find({
|
||||
collection: 'media',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
where: { filename: { equals: filename } },
|
||||
})
|
||||
|
||||
if (exact.docs[0]) return exact.docs[0]
|
||||
|
||||
const ext = path.extname(filename)
|
||||
const basename = path.basename(filename, ext)
|
||||
const suffixedFilename = new RegExp(`^${escapeRegExp(basename)}-\\d+${escapeRegExp(ext)}$`)
|
||||
|
||||
const candidates = await payload.find({
|
||||
collection: 'media',
|
||||
depth: 0,
|
||||
limit: 100,
|
||||
pagination: false,
|
||||
sort: '-createdAt',
|
||||
where: { filename: { contains: basename } },
|
||||
})
|
||||
|
||||
return candidates.docs.find((doc) => doc.filename && suffixedFilename.test(doc.filename))
|
||||
}
|
||||
|
||||
export async function mediaByFilename(payload: Payload, filename: string) {
|
||||
return (await findMediaByFilename(payload, filename))?.id
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'dotenv/config'
|
||||
import fs from 'fs/promises'
|
||||
import path from 'path'
|
||||
import { getPayload, type File, type Payload } from 'payload'
|
||||
import { findMediaByFilename } from './mediaByFilename'
|
||||
|
||||
const root = process.cwd()
|
||||
const spaDir = path.join(root, 'src/spa')
|
||||
@@ -132,13 +133,7 @@ function altForSource(source: string) {
|
||||
}
|
||||
|
||||
async function findExistingMedia(payload: Payload, filename: string) {
|
||||
const existing = await payload.find({
|
||||
collection: 'media',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
where: { filename: { equals: filename } },
|
||||
})
|
||||
return existing.docs[0]
|
||||
return findMediaByFilename(payload, filename)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { events } from '@/spa/data/events'
|
||||
import { preistraegerDetailContent } from '@/spa/preistraegerDetailContent'
|
||||
import { WINNERS } from '@/spa/data/winners'
|
||||
import { spaPages, type SpaPageRegistryEntry } from '@/spa/pageRegistry'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
type Payload = Awaited<ReturnType<typeof getPayload>>
|
||||
type TargetCollection = 'pages' | 'posts' | 'events' | 'preistraeger'
|
||||
@@ -96,25 +97,7 @@ function baseData(page: SpaPageRegistryEntry) {
|
||||
}
|
||||
|
||||
function pageData(page: SpaPageRegistryEntry) {
|
||||
return {
|
||||
...baseData(page),
|
||||
hero: {
|
||||
type: 'none' as const,
|
||||
},
|
||||
layout: [
|
||||
{
|
||||
blockType: 'content' as const,
|
||||
columns: [
|
||||
{
|
||||
size: 'full' as const,
|
||||
richText: emptyRichText(
|
||||
`Diese Seite wird aktuell noch von der migrierten SPA gerendert. Über „Preview“ kann der aktuelle Stand unter ${page.path} betrachtet werden.`,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
return baseData(page)
|
||||
}
|
||||
|
||||
function postData(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
|
||||
@@ -299,15 +282,8 @@ async function loadMediaMap(payload: Payload): Promise<MediaMap> {
|
||||
])
|
||||
const assets = await Promise.all(
|
||||
[...sources].map(async (source) => {
|
||||
const existing = await payload.find({
|
||||
collection: 'media',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
pagination: false,
|
||||
where: { filename: { equals: filenameForSource(source) } },
|
||||
})
|
||||
const media = existing.docs[0]
|
||||
return media ? ([source, media.id] as const) : undefined
|
||||
const media = await mediaByFilename(payload, filenameForSource(source))
|
||||
return media ? ([source, media] as const) : undefined
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -320,7 +296,12 @@ async function main() {
|
||||
payload.logger.info(`Migrating ${spaPages.length} SPA routes into Payload collections...`)
|
||||
|
||||
for (const page of spaPages) {
|
||||
try {
|
||||
await upsertPage(payload, page, mediaMap)
|
||||
} catch (error) {
|
||||
payload.logger.error({ msg: `Failed to migrate SPA route ${page.path} into ${targetForPage(page)}`, err: error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
payload.logger.info('SPA page migration complete.')
|
||||
|
||||
@@ -3,19 +3,10 @@ import 'dotenv/config'
|
||||
import config from '@payload-config'
|
||||
import crypto from 'crypto'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { aboutContent } from '@/spa/aboutContent'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const safeRemoteName = (url: string) => {
|
||||
const parsed = new URL(url)
|
||||
const ext = parsed.pathname.split('/').pop()?.includes('.') ? `.${parsed.pathname.split('.').pop()}` : '.jpg'
|
||||
@@ -104,6 +95,7 @@ async function main() {
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded About CMS fields for page ${page.id}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -2,19 +2,10 @@ import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
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)
|
||||
@@ -47,6 +38,7 @@ async function main() {
|
||||
)
|
||||
|
||||
payload.logger.info(`Preloaded Blog detail CMS fields for ${posts.docs.length} records`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -2,19 +2,10 @@ import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { contactContent } from '@/spa/contactContent'
|
||||
|
||||
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 })
|
||||
|
||||
@@ -61,6 +52,7 @@ async function main() {
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded contact CMS fields for page ${page.id}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -5,6 +5,14 @@ import { getPayload } from 'payload'
|
||||
|
||||
import { datenschutzContent } from '@/spa/datenschutzContent'
|
||||
|
||||
const contentForPayload = {
|
||||
...datenschutzContent,
|
||||
sections: datenschutzContent.sections.map(({ id, ...section }) => ({
|
||||
...section,
|
||||
anchorId: id,
|
||||
})),
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
@@ -28,11 +36,12 @@ async function main() {
|
||||
overrideAccess: true,
|
||||
context: { disableRevalidate: true },
|
||||
data: {
|
||||
datenschutz: datenschutzContent,
|
||||
datenschutz: contentForPayload,
|
||||
} as never,
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded Datenschutz CMS fields for page ${page.id}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -2,19 +2,10 @@ import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { eventDetailContent } from '@/spa/eventDetailContent'
|
||||
|
||||
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, eventDetailContent.fallbackImageFilename)
|
||||
@@ -47,6 +38,7 @@ async function main() {
|
||||
)
|
||||
|
||||
payload.logger.info(`Preloaded Event detail CMS fields for ${events.docs.length} records`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { formularUploadContent } from '@/spa/formularUploadContent'
|
||||
|
||||
@@ -33,6 +34,7 @@ async function main() {
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded Formular upload CMS fields for page ${page.id}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
67
src/scripts/preload-globals-cms.ts
Normal file
67
src/scripts/preload-globals-cms.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { defaultFooterData, defaultHeaderData } from '@/spa/cmsNavigation'
|
||||
import { defaultSiteSettings } from '@/spa/siteSettings'
|
||||
|
||||
async function main() {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
const [logo, ewifLogo, partnerLogos, skylineImage] = await Promise.all([
|
||||
mediaByFilename(payload, 'bmp-logo.png'),
|
||||
mediaByFilename(payload, 'ewif-logo.png'),
|
||||
mediaByFilename(payload, 'partner-logos.png'),
|
||||
mediaByFilename(payload, 'munich-skyline.jpg'),
|
||||
])
|
||||
|
||||
const { logo: _headerLogo, languages, ...headerData } = defaultHeaderData
|
||||
const { logo: _footerLogo, partnerLogos: _defaultPartnerLogos, ...footerData } = defaultFooterData
|
||||
const { skylineImageUrl: _skylineImageUrl, ...siteSettingsData } = defaultSiteSettings
|
||||
|
||||
await Promise.all([
|
||||
payload.updateGlobal({
|
||||
slug: 'header',
|
||||
context: { disableRevalidate: true },
|
||||
data: {
|
||||
...headerData,
|
||||
logo,
|
||||
languages: languages.map((label) => ({ label })),
|
||||
} as never,
|
||||
}),
|
||||
payload.updateGlobal({
|
||||
slug: 'footer',
|
||||
context: { disableRevalidate: true },
|
||||
data: {
|
||||
...footerData,
|
||||
logo,
|
||||
partnerLogos: [
|
||||
{ alt: 'EWIF – Europäisches Wirtschaftsforum', image: ewifLogo },
|
||||
{
|
||||
alt: 'Hochschule für angewandtes Management · Gipfeldialog Altaussee',
|
||||
image: partnerLogos,
|
||||
},
|
||||
].filter((item) => item.image),
|
||||
} as never,
|
||||
}),
|
||||
payload.updateGlobal({
|
||||
slug: 'site-settings',
|
||||
context: { disableRevalidate: true },
|
||||
data: {
|
||||
...siteSettingsData,
|
||||
skylineImage,
|
||||
} as never,
|
||||
}),
|
||||
])
|
||||
|
||||
payload.logger.info('Preloaded global CMS settings.')
|
||||
await payload.destroy()
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -3,19 +3,10 @@ import 'dotenv/config'
|
||||
import config from '@payload-config'
|
||||
import crypto from 'crypto'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { homeContent } from '@/spa/homeContent'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const safeRemoteName = (url: string) => {
|
||||
const parsed = new URL(url)
|
||||
const ext = parsed.pathname.split('/').pop()?.includes('.') ? `.${parsed.pathname.split('.').pop()}` : '.jpg'
|
||||
@@ -115,6 +106,7 @@ async function main() {
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded home CMS fields for page ${page.id}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -5,6 +5,14 @@ import { getPayload } from 'payload'
|
||||
|
||||
import { impressumContent } from '@/spa/impressumContent'
|
||||
|
||||
const contentForPayload = {
|
||||
...impressumContent,
|
||||
sections: impressumContent.sections.map(({ id, ...section }) => ({
|
||||
...section,
|
||||
anchorId: id,
|
||||
})),
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
@@ -28,11 +36,12 @@ async function main() {
|
||||
overrideAccess: true,
|
||||
context: { disableRevalidate: true },
|
||||
data: {
|
||||
impressum: impressumContent,
|
||||
impressum: contentForPayload,
|
||||
} as never,
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded Impressum CMS fields for page ${page.id}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -2,19 +2,10 @@ import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { mitgliedWerdenContent } from '@/spa/mitgliedWerdenContent'
|
||||
|
||||
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 })
|
||||
|
||||
@@ -42,9 +33,10 @@ async function main() {
|
||||
const formImage = await mediaByFilename(payload, mitgliedWerdenContent.formIntro.imageFilename)
|
||||
const applicationItems = await Promise.all(
|
||||
mitgliedWerdenContent.applicationOptions.items.map(async (item) => {
|
||||
const { fileFilename, ...rest } = item
|
||||
const { fileFilename, id, ...rest } = item
|
||||
return {
|
||||
...rest,
|
||||
stableId: id,
|
||||
file: fileFilename ? await mediaByFilename(payload, fileFilename) : undefined,
|
||||
}
|
||||
}),
|
||||
@@ -52,6 +44,10 @@ async function main() {
|
||||
|
||||
const { imageFilename: _heroFilename, ...heroContent } = mitgliedWerdenContent.hero
|
||||
const { imageFilename: _formFilename, ...formIntroContent } = mitgliedWerdenContent.formIntro
|
||||
const aboutContent = {
|
||||
...mitgliedWerdenContent.about,
|
||||
paragraphs: mitgliedWerdenContent.about.paragraphs.map((text) => ({ text })),
|
||||
}
|
||||
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
@@ -65,6 +61,7 @@ async function main() {
|
||||
...heroContent,
|
||||
image: heroImage,
|
||||
},
|
||||
about: aboutContent,
|
||||
applicationOptions: {
|
||||
...mitgliedWerdenContent.applicationOptions,
|
||||
items: applicationItems,
|
||||
@@ -78,6 +75,7 @@ async function main() {
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded Mitglied werden CMS fields for page ${page.id}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -3,19 +3,10 @@ import 'dotenv/config'
|
||||
import config from '@payload-config'
|
||||
import crypto from 'crypto'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { netzwerkContent } from '@/spa/netzwerkContent'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const safeRemoteName = (url: string) => {
|
||||
const parsed = new URL(url)
|
||||
const ext = parsed.pathname.split('/').pop()?.includes('.') ? `.${parsed.pathname.split('.').pop()}` : '.jpg'
|
||||
@@ -39,6 +30,11 @@ const itemsWithImageUploads = async <T extends { image?: string }>(payload: Payl
|
||||
}),
|
||||
)
|
||||
|
||||
const partnersForPayload = netzwerkContent.partners.items.map(({ id, ...partner }) => ({
|
||||
...partner,
|
||||
stableId: id,
|
||||
}))
|
||||
|
||||
async function main() {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
@@ -89,11 +85,16 @@ async function main() {
|
||||
...sponsoringContent,
|
||||
image: sponsoringImage,
|
||||
},
|
||||
partners: {
|
||||
...netzwerkContent.partners,
|
||||
items: partnersForPayload,
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded Netzwerk CMS fields for page ${page.id}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -2,19 +2,10 @@ import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { participationContent } from '@/spa/participationContent'
|
||||
|
||||
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 })
|
||||
|
||||
@@ -70,6 +61,7 @@ async function main() {
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded participation CMS fields for page ${page.id}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -2,19 +2,10 @@ import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { preistraegerDetailContent } from '@/spa/preistraegerDetailContent'
|
||||
|
||||
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, sideImage] = await Promise.all([
|
||||
@@ -54,6 +45,7 @@ async function main() {
|
||||
)
|
||||
|
||||
payload.logger.info(`Preloaded Preisträger detail CMS fields for ${winners.docs.length} records`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -2,19 +2,10 @@ import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
import { preistraegerIndexContent } from '@/spa/preistraegerIndexContent'
|
||||
|
||||
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, preistraegerIndexContent.card.fallbackImageFilename)
|
||||
@@ -51,6 +42,7 @@ async function main() {
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded Preisträger index CMS fields for page ${page.id}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
@@ -2,19 +2,10 @@ import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { getPayload, type Payload } from 'payload'
|
||||
import { mediaByFilename } from './mediaByFilename'
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
const filenameFromPath = (source: string) => source.split('?')[0]?.split('/').filter(Boolean).pop()
|
||||
|
||||
const fallbackItemsWithImages = async <T extends { img?: string }>(
|
||||
@@ -93,6 +84,7 @@ async function main() {
|
||||
})
|
||||
|
||||
payload.logger.info(`Preloaded Press index CMS fields for page ${page.id}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
|
||||
116
src/scripts/setup-fresh-database.ts
Normal file
116
src/scripts/setup-fresh-database.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'dotenv/config'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
const orderedScripts = [
|
||||
{
|
||||
label: 'Import media assets',
|
||||
path: 'src/scripts/migrate-spa-media.ts',
|
||||
},
|
||||
{
|
||||
label: 'Create base pages, posts, events, and Preisträger records',
|
||||
path: 'src/scripts/migrate-spa-pages.ts',
|
||||
},
|
||||
{
|
||||
label: 'Store header, footer, and site settings defaults',
|
||||
path: 'src/scripts/preload-globals-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload detail page copy for blog posts',
|
||||
path: 'src/scripts/preload-blog-detail-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload detail page copy for events',
|
||||
path: 'src/scripts/preload-event-detail-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload detail page copy for Preisträger',
|
||||
path: 'src/scripts/preload-preistraeger-detail-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload home page',
|
||||
path: 'src/scripts/preload-home-page-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload about page',
|
||||
path: 'src/scripts/preload-about-page-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload contact page',
|
||||
path: 'src/scripts/preload-contact-page-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload Datenschutz page',
|
||||
path: 'src/scripts/preload-datenschutz-page-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload formular upload page',
|
||||
path: 'src/scripts/preload-formular-upload-page-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload Impressum page',
|
||||
path: 'src/scripts/preload-impressum-page-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload Mitglied werden page',
|
||||
path: 'src/scripts/preload-mitglied-werden-page-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload Netzwerk page',
|
||||
path: 'src/scripts/preload-netzwerk-page-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload participation page',
|
||||
path: 'src/scripts/preload-participation-page-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload Preisträger index page',
|
||||
path: 'src/scripts/preload-preistraeger-index-page-cms.ts',
|
||||
},
|
||||
{
|
||||
label: 'Preload press index page',
|
||||
path: 'src/scripts/preload-press-index-page-cms.ts',
|
||||
},
|
||||
]
|
||||
|
||||
async function initializeSchema() {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
payload.logger.info(
|
||||
`Initialized Payload schema for ${Object.keys(payload.collections).length} collections and ${
|
||||
Object.keys(payload.globals.config).length
|
||||
} globals.`,
|
||||
)
|
||||
|
||||
await payload.destroy()
|
||||
}
|
||||
|
||||
function runScript(scriptPath: string) {
|
||||
const result = spawnSync('tsx', [scriptPath], {
|
||||
env: process.env,
|
||||
stdio: 'inherit',
|
||||
})
|
||||
|
||||
if (result.error) throw result.error
|
||||
if (result.status !== 0) throw new Error(`${scriptPath} failed with exit code ${result.status ?? 'unknown'}`)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Setting up a fresh Payload database...')
|
||||
console.log('1. Initialize Payload tables')
|
||||
await initializeSchema()
|
||||
|
||||
for (const [index, script] of orderedScripts.entries()) {
|
||||
console.log(`${index + 2}. ${script.label}`)
|
||||
runScript(script.path)
|
||||
}
|
||||
|
||||
console.log('Fresh database setup complete.')
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -29,6 +29,9 @@ export type LegalPageContent = {
|
||||
}
|
||||
|
||||
type LegalPageCms = Partial<LegalPageContent>
|
||||
type LegalSectionCms = LegalSectionContent & {
|
||||
anchorId?: string
|
||||
}
|
||||
|
||||
const fallbackText = (value: unknown, fallback: string) => (typeof value === 'string' && value.length > 0 ? value : fallback)
|
||||
|
||||
@@ -42,7 +45,10 @@ export default function LegalPage({ content, cmsKey }: { content: LegalPageConte
|
||||
const cms = (useCmsRoute()?.doc?.[cmsKey] || {}) as LegalPageCms
|
||||
const hero = { ...content.hero, ...(cms.hero || {}) }
|
||||
const navigation = { ...content.navigation, ...(cms.navigation || {}) }
|
||||
const sections = fallbackArray<LegalSectionContent>(cms.sections, content.sections)
|
||||
const sections = fallbackArray<LegalSectionCms>(cms.sections, content.sections).map((section) => ({
|
||||
...section,
|
||||
id: section.anchorId || section.id,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div style={{ background: CREAM, minHeight: '100vh' }}>
|
||||
|
||||
@@ -90,7 +90,7 @@ export const homeContent = {
|
||||
headline: 'DIE JURY BEWERTET GERADE.',
|
||||
body:
|
||||
'Die Anmeldephase 2026 ist geschlossen. Unsere unabhängige Expertenjury aus Wissenschaft und Praxis sichtet und bewertet derzeit alle Einreichungen.',
|
||||
cta: null,
|
||||
cta: {},
|
||||
accent: '#4A8FC9',
|
||||
bg: '#060D1C',
|
||||
glow: 'rgba(74,143,201,0.12)',
|
||||
|
||||
@@ -596,7 +596,7 @@ function StatusSlider({ data }: { data?: HomeCms }) {
|
||||
const phases = fallbackArray(data?.phases, homeContent.status.phases).map((phase, i) => {
|
||||
const phaseRecord = phase as StatusPhase
|
||||
const fallbackPhase = homeContent.status.phases[i]
|
||||
const fallbackCta = fallbackPhase?.cta
|
||||
const fallbackCta = fallbackPhase?.cta?.label
|
||||
? { label: fallbackPhase.cta.label, to: fallbackPhase.cta.url }
|
||||
: null
|
||||
const cta = phaseRecord?.cta?.label
|
||||
|
||||
@@ -30,7 +30,7 @@ type NetzwerkCms = Partial<typeof netzwerkContent> & {
|
||||
|
||||
type PatronPerson = (typeof netzwerkContent.patronage.people)[number] & { imageUpload?: unknown }
|
||||
type JuryMember = (typeof netzwerkContent.jury.members)[number] & { imageUpload?: unknown }
|
||||
type Partner = (typeof netzwerkContent.partners.items)[number] & { tier: 1 | 2 | 3; heroImg?: string; linkedin?: string; logoSubtext?: string }
|
||||
type Partner = (typeof netzwerkContent.partners.items)[number] & { stableId?: string; tier: 1 | 2 | 3; heroImg?: string; linkedin?: string; logoSubtext?: string }
|
||||
type PartnerTier = (typeof netzwerkContent.partners.tiers)[number]
|
||||
type ExpertiseRow = (typeof netzwerkContent.expertise.rows)[number]
|
||||
type StatItem = (typeof netzwerkContent.juryIntro.stats)[number]
|
||||
@@ -458,6 +458,7 @@ const Netzwerk: React.FC = () => {
|
||||
const defaultLogoColor = fallbackText(partnerSection.defaultLogoColor, netzwerkContent.partners.defaultLogoColor);
|
||||
const partners = fallbackArray<Partner>(partnerSection.items, netzwerkContent.partners.items as Partner[]).map((partner) => ({
|
||||
...partner,
|
||||
id: partner.stableId || partner.id,
|
||||
logoColor: fallbackText(partner.logoColor, defaultLogoColor),
|
||||
}));
|
||||
const partnerTiers = fallbackArray<PartnerTier>(partnerSection.tiers, netzwerkContent.partners.tiers);
|
||||
|
||||
Reference in New Issue
Block a user