305 lines
8.4 KiB
TypeScript
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)
|
|
})
|