Files
bmp-website-2026/src/scripts/migrate-spa-pages.ts
2026-06-17 17:15:45 +02:00

305 lines
8.4 KiB
TypeScript

import config from '@payload-config'
import crypto from 'crypto'
import path from 'path'
import 'dotenv/config'
import { getPayload } from 'payload'
import { articles } from '@/spa/data/blog'
import { events } from '@/spa/data/events'
import { WINNERS } from '@/spa/data/winners'
import { spaPages, type SpaPageRegistryEntry } from '@/spa/pageRegistry'
type Payload = Awaited<ReturnType<typeof getPayload>>
type TargetCollection = 'pages' | 'posts' | 'events' | 'preistraeger'
const emptyRichText = (text: string) => ({
root: {
type: 'root',
children: [
{
type: 'paragraph',
children: [
{
type: 'text',
detail: 0,
format: 0,
mode: 'normal',
style: '',
text,
version: 1,
},
],
direction: 'ltr' as const,
format: '' as const,
indent: 0,
version: 1,
},
],
direction: 'ltr' as const,
format: '' as const,
indent: 0,
version: 1,
},
})
function targetForPage(page: SpaPageRegistryEntry): TargetCollection {
if (page.source === 'winner') return 'preistraeger'
if (page.source === 'event') return 'events'
if (page.source === 'blog') return 'posts'
return 'pages'
}
function targetSlug(page: SpaPageRegistryEntry) {
if (page.source === 'winner' || page.source === 'event' || page.source === 'blog') {
return page.path.split('/').filter(Boolean).at(-1) || page.slug
}
return page.slug
}
function legacySanitizedSlug(page: SpaPageRegistryEntry) {
if (page.path === '/') return 'startseite'
return page.path.replace(/^\//, '').replace(/\//g, '')
}
type MediaMap = Map<string, number | string>
function safeRemoteName(url: string) {
const parsed = new URL(url)
const ext = path.extname(parsed.pathname) || '.jpg'
const hash = crypto.createHash('sha1').update(url).digest('hex').slice(0, 10)
return `${parsed.hostname.replace(/[^a-z0-9]+/gi, '-')}-${hash}${ext}`.toLowerCase()
}
function filenameForSource(source: string) {
return source.startsWith('http') ? safeRemoteName(source) : path.basename(source.split('?')[0])
}
function mediaID(mediaMap: MediaMap, source?: string) {
return source ? mediaMap.get(source) : undefined
}
function baseData(page: SpaPageRegistryEntry) {
return {
title: page.title,
slug: targetSlug(page),
spaPath: page.path,
_status: 'published' as const,
meta: {
title: page.title,
description: page.description,
},
publishedAt: new Date().toISOString(),
}
}
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.`,
),
},
],
},
],
}
}
function postData(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
const article = articles.find((item) => item.slug === targetSlug(page))
return {
...baseData(page),
heroImage: mediaID(mediaMap, article?.img),
excerpt: article?.excerpt || page.description,
cat: article?.cat,
date: article?.date,
authorName: article?.author,
authorRole: article?.authorRole,
readTime: article?.readTime,
sections: article?.sections || [],
tags: article?.tags?.map((tag) => ({ tag })) || [],
relatedSlugs: article?.relatedSlugs?.map((slug) => ({ slug })) || [],
content: emptyRichText(
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.`,
),
relatedPosts: [],
categories: [],
authors: [],
}
}
function eventData(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
const event = events.find((item) => item.slug === targetSlug(page))
return {
...baseData(page),
subtitle: event?.subtitle,
image: mediaID(mediaMap, event?.img),
description: event?.description || page.description,
longDescription: event?.longDescription,
date: event?.date,
time: event?.time,
location: event?.location,
venue: event?.venue,
category: event?.category,
status: event?.status || 'geplant',
eckdaten: event?.eckdaten || [],
updates: event?.updates?.map(({ id: _id, ...update }) => update) || [],
}
}
function preistraegerData(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
const winner = WINNERS.find((item) => item.slug === targetSlug(page))
return {
...baseData(page),
image: mediaID(mediaMap, winner?.img),
category: winner?.category,
year: winner?.year,
awardType: winner?.type,
shortDesc: winner?.shortDesc || page.description,
longDesc: winner?.longDesc,
description: winner?.shortDesc || page.description,
quote: winner?.quote,
quotePerson: winner?.quotePerson,
quoteRole: winner?.quoteRole,
location: winner?.location,
industry: winner?.industry,
website: winner?.website,
hasMedia: winner?.hasMedia,
}
}
function dataForPage(page: SpaPageRegistryEntry, mediaMap: MediaMap) {
const target = targetForPage(page)
if (target === 'pages') return pageData(page)
if (target === 'posts') return postData(page, mediaMap)
if (target === 'events') return eventData(page, mediaMap)
return preistraegerData(page, mediaMap)
}
async function findExistingDoc(payload: Payload, collection: TargetCollection, page: SpaPageRegistryEntry) {
const existing = await payload.find({
collection: collection as any,
depth: 0,
limit: 1,
pagination: false,
where: {
or: [
{
spaPath: {
equals: page.path,
},
},
{
slug: {
equals: targetSlug(page),
},
},
{
slug: {
equals: page.slug,
},
},
{
slug: {
equals: legacySanitizedSlug(page),
},
},
],
},
})
return existing.docs[0]
}
async function removeMovedPageDoc(payload: Payload, page: SpaPageRegistryEntry) {
const pageDoc = await findExistingDoc(payload, 'pages', page)
if (!pageDoc) return
await payload.delete({
collection: 'pages',
id: pageDoc.id,
depth: 0,
context: { disableRevalidate: true },
})
}
async function upsertPage(payload: Payload, page: SpaPageRegistryEntry, mediaMap: MediaMap) {
const collection = targetForPage(page)
const existing = await findExistingDoc(payload, collection, page)
const data = dataForPage(page, mediaMap)
if (existing) {
await payload.update({
collection: collection as any,
id: existing.id,
data,
depth: 0,
context: { disableRevalidate: true },
})
payload.logger.info(`updated ${collection}: ${page.path}`)
} else {
await payload.create({
collection: collection as any,
data,
depth: 0,
context: { disableRevalidate: true },
})
payload.logger.info(`created ${collection}: ${page.path}`)
}
if (collection !== 'pages') {
await removeMovedPageDoc(payload, page)
}
}
async function loadMediaMap(payload: Payload): Promise<MediaMap> {
const sources = new Set([
...WINNERS.map((winner) => winner.img),
...events.map((event) => event.img),
...articles.map((article) => article.img),
])
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
}),
)
return new Map(assets.filter(Boolean) as [string, number | string][])
}
async function main() {
const payload = await getPayload({ config })
const mediaMap = await loadMediaMap(payload)
payload.logger.info(`Migrating ${spaPages.length} SPA routes into Payload collections...`)
for (const page of spaPages) {
await upsertPage(payload, page, mediaMap)
}
payload.logger.info('SPA page migration complete.')
process.exit(0)
}
main().catch((error) => {
console.error(error)
process.exit(1)
})