This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* One-shot migration script: TinaCMS MDX files → Sanity documents
|
||||
*
|
||||
* Prerequisites:
|
||||
* npm install --save-dev gray-matter @portabletext/markdown
|
||||
*
|
||||
* Usage:
|
||||
* npm run migrate
|
||||
*
|
||||
* Revoke SANITY_API_TOKEN after migration is complete.
|
||||
*/
|
||||
|
||||
import { createClient } from '@sanity/client'
|
||||
import { createReadStream, readdirSync, readFileSync, existsSync } from 'fs'
|
||||
import { join, basename } from 'path'
|
||||
import matter from 'gray-matter'
|
||||
import { markdownToPortableText } from '@portabletext/markdown'
|
||||
|
||||
const PROJECT_ID = process.env.PUBLIC_SANITY_PROJECT_ID
|
||||
const TOKEN = process.env.SANITY_API_TOKEN
|
||||
const DATASET = process.env.PUBLIC_SANITY_DATASET ?? 'production'
|
||||
|
||||
if (!PROJECT_ID || !TOKEN) {
|
||||
console.error('Missing PUBLIC_SANITY_PROJECT_ID or SANITY_API_TOKEN environment variables.')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const client = createClient({
|
||||
projectId: PROJECT_ID,
|
||||
dataset: DATASET,
|
||||
apiVersion: '2024-01-01',
|
||||
token: TOKEN,
|
||||
useCdn: false,
|
||||
})
|
||||
|
||||
function randomKey() {
|
||||
return Math.random().toString(36).slice(2, 10)
|
||||
}
|
||||
|
||||
function emptyPortableText() {
|
||||
return []
|
||||
}
|
||||
|
||||
function stringToPortableText(text) {
|
||||
if (!text || text === '') return emptyPortableText()
|
||||
return [
|
||||
{
|
||||
_type: 'block',
|
||||
_key: randomKey(),
|
||||
style: 'normal',
|
||||
markDefs: [],
|
||||
children: [{ _type: 'span', _key: randomKey(), text, marks: [] }],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
// Upload a local image file to Sanity and return a Sanity image reference
|
||||
async function uploadImageFile(localPath, label) {
|
||||
if (!existsSync(localPath)) {
|
||||
console.warn(` Skipping missing image: ${localPath}`)
|
||||
return null
|
||||
}
|
||||
console.log(` Uploading image: ${label}`)
|
||||
const asset = await client.assets.upload('image', createReadStream(localPath), {
|
||||
filename: basename(localPath),
|
||||
})
|
||||
return { _type: 'image', asset: { _type: 'reference', _ref: asset._id } }
|
||||
}
|
||||
|
||||
// Map /images/filename.ext → public/images/filename.ext (relative to project root)
|
||||
function localPathForImageUrl(imageUrl) {
|
||||
if (!imageUrl || !imageUrl.startsWith('/images/')) return null
|
||||
return join('public', imageUrl)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Migrate theaterHomepage singleton
|
||||
// ---------------------------------------------------------------------------
|
||||
async function migrateTheaterHomepage() {
|
||||
console.log('\n--- Migrating theaterHomepage ---')
|
||||
const raw = readFileSync('content/theater/homepage.mdx', 'utf-8')
|
||||
const { data: fm } = matter(raw)
|
||||
|
||||
// Pre-upload all organization logos
|
||||
const orgs = (fm.team?.organizations ?? [])
|
||||
const uploadedLogos = {}
|
||||
for (const org of orgs) {
|
||||
if (org.logo && !uploadedLogos[org.logo]) {
|
||||
const localPath = localPathForImageUrl(org.logo)
|
||||
if (localPath) {
|
||||
uploadedLogos[org.logo] = await uploadImageFile(localPath, org.logo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const doc = {
|
||||
_type: 'theaterHomepage',
|
||||
_id: 'theaterHomepage',
|
||||
title: fm.title,
|
||||
hero: fm.hero ?? {},
|
||||
projekt: {
|
||||
title_de: fm.projekt?.title_de ?? '',
|
||||
title_en: fm.projekt?.title_en ?? '',
|
||||
content_de: stringToPortableText(fm.projekt?.content_de),
|
||||
content_en: stringToPortableText(fm.projekt?.content_en),
|
||||
},
|
||||
team: {
|
||||
title_de: fm.team?.title_de ?? '',
|
||||
title_en: fm.team?.title_en ?? '',
|
||||
content_de: stringToPortableText(fm.team?.content_de),
|
||||
content_en: stringToPortableText(fm.team?.content_en),
|
||||
organizations: orgs.map((org) => ({
|
||||
_key: randomKey(),
|
||||
name: org.name,
|
||||
role_de: org.role_de,
|
||||
role_en: org.role_en,
|
||||
url: org.url,
|
||||
...(org.logo && uploadedLogos[org.logo] ? { logo: uploadedLogos[org.logo] } : {}),
|
||||
})),
|
||||
},
|
||||
stueck: {
|
||||
title_de: fm.stueck?.title_de ?? '',
|
||||
title_en: fm.stueck?.title_en ?? '',
|
||||
content_de: stringToPortableText(fm.stueck?.content_de),
|
||||
content_en: stringToPortableText(fm.stueck?.content_en),
|
||||
},
|
||||
mitwirkung: {
|
||||
title_de: fm.mitwirkung?.title_de ?? '',
|
||||
title_en: fm.mitwirkung?.title_en ?? '',
|
||||
content_de: stringToPortableText(fm.mitwirkung?.content_de),
|
||||
content_en: stringToPortableText(fm.mitwirkung?.content_en),
|
||||
director_name: fm.mitwirkung?.director_name,
|
||||
director_email: fm.mitwirkung?.director_email,
|
||||
director_phone: fm.mitwirkung?.director_phone,
|
||||
},
|
||||
termine: {
|
||||
title_de: fm.termine?.title_de ?? '',
|
||||
title_en: fm.termine?.title_en ?? '',
|
||||
events: (fm.termine?.events ?? []).map((e) => ({ ...e, _key: randomKey() })),
|
||||
},
|
||||
presse: {
|
||||
title_de: fm.presse?.title_de ?? '',
|
||||
title_en: fm.presse?.title_en ?? '',
|
||||
content_de: stringToPortableText(fm.presse?.content_de),
|
||||
content_en: stringToPortableText(fm.presse?.content_en),
|
||||
},
|
||||
gallery: {
|
||||
title_de: fm.gallery?.title_de ?? '',
|
||||
title_en: fm.gallery?.title_en ?? '',
|
||||
images: [],
|
||||
},
|
||||
tickets: {
|
||||
title_de: fm.tickets?.title_de ?? '',
|
||||
title_en: fm.tickets?.title_en ?? '',
|
||||
content_de: stringToPortableText(fm.tickets?.content_de),
|
||||
content_en: stringToPortableText(fm.tickets?.content_en),
|
||||
...(fm.tickets?.ticket_url ? { ticket_url: fm.tickets.ticket_url } : {}),
|
||||
},
|
||||
}
|
||||
|
||||
await client.createOrReplace(doc)
|
||||
console.log(' ✓ theaterHomepage created/replaced')
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Migrate blog posts
|
||||
// ---------------------------------------------------------------------------
|
||||
async function migratePosts() {
|
||||
console.log('\n--- Migrating blog posts ---')
|
||||
const blogDir = 'content/blog'
|
||||
if (!existsSync(blogDir)) {
|
||||
console.log(' No blog directory found, skipping.')
|
||||
return
|
||||
}
|
||||
const files = readdirSync(blogDir).filter((f) => f.endsWith('.mdx'))
|
||||
|
||||
for (const file of files) {
|
||||
const slug = file.replace('.mdx', '')
|
||||
const raw = readFileSync(join(blogDir, file), 'utf-8')
|
||||
const { data: fm, content: body } = matter(raw)
|
||||
|
||||
const portableTextBody = body.trim()
|
||||
? markdownToPortableText(body)
|
||||
: []
|
||||
|
||||
// Upload cover image if present and exists locally
|
||||
let coverImage
|
||||
if (fm.coverImage) {
|
||||
const localPath = localPathForImageUrl(fm.coverImage)
|
||||
if (localPath) {
|
||||
coverImage = await uploadImageFile(localPath, fm.coverImage)
|
||||
}
|
||||
}
|
||||
|
||||
const doc = {
|
||||
_type: 'post',
|
||||
_id: `post-${slug}`,
|
||||
title: fm.title,
|
||||
slug: { _type: 'slug', current: slug },
|
||||
date: fm.date,
|
||||
excerpt: fm.excerpt,
|
||||
body: portableTextBody,
|
||||
...(coverImage ? { coverImage } : {}),
|
||||
}
|
||||
|
||||
await client.createOrReplace(doc)
|
||||
console.log(` ✓ Post migrated: ${slug}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Migrate static pages
|
||||
// ---------------------------------------------------------------------------
|
||||
async function migratePages() {
|
||||
console.log('\n--- Migrating pages ---')
|
||||
const pagesDir = 'content/pages'
|
||||
if (!existsSync(pagesDir)) {
|
||||
console.log(' No pages directory found, skipping.')
|
||||
return
|
||||
}
|
||||
const files = readdirSync(pagesDir).filter((f) => f.endsWith('.mdx'))
|
||||
|
||||
for (const file of files) {
|
||||
const slug = file.replace('.mdx', '')
|
||||
const raw = readFileSync(join(pagesDir, file), 'utf-8')
|
||||
const { data: fm, content: body } = matter(raw)
|
||||
|
||||
const portableTextBody = body.trim()
|
||||
? markdownToPortableText(body)
|
||||
: []
|
||||
|
||||
const doc = {
|
||||
_type: 'page',
|
||||
_id: `page-${slug}`,
|
||||
title: fm.title,
|
||||
slug: { _type: 'slug', current: slug },
|
||||
description: fm.description,
|
||||
body: portableTextBody,
|
||||
}
|
||||
|
||||
await client.createOrReplace(doc)
|
||||
console.log(` ✓ Page migrated: ${slug}`)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Run
|
||||
// ---------------------------------------------------------------------------
|
||||
try {
|
||||
await migrateTheaterHomepage()
|
||||
await migratePosts()
|
||||
await migratePages()
|
||||
console.log('\n✓ Migration complete.')
|
||||
} catch (err) {
|
||||
console.error('\n✗ Migration failed:', err)
|
||||
process.exit(1)
|
||||
}
|
||||
Reference in New Issue
Block a user