/** * 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) }