diff --git a/.env b/.env
new file mode 100644
index 0000000..8c76213
--- /dev/null
+++ b/.env
@@ -0,0 +1,9 @@
+# Sanity CMS Configuration
+# Get your project ID from https://sanity.io/manage
+PUBLIC_SANITY_PROJECT_ID=p49jotqu
+PUBLIC_SANITY_DATASET=production
+
+# Only needed when running the migration script (scripts/migrate-to-sanity.mjs)
+# Create a write token at https://sanity.io/manage → API → Tokens
+# Revoke after migration is complete.
+SANITY_API_TOKEN=skQ3tzSF8tp5ETeDq26zlgYAoxv50L4D37uITONtQ0efe4wHxjH64NrauljoyucMGy0F58fOXmUbQXh7C0glLFcD0PUd0tVRwzifuuLYvlZmX0MKOo6aZB2U2P5ISydRNzKd0sPGyMjxRo47psGb3wCtzT0Xcv8ByMVOlqnn35bc6uDj7Afl
diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml
index 87f808e..fe016e5 100644
--- a/.gitea/workflows/build.yaml
+++ b/.gitea/workflows/build.yaml
@@ -39,16 +39,6 @@ jobs:
${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}:${{ steps.tag.outputs.sha }}
${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}:latest
- - name: Build and push backend image
- uses: docker/build-push-action@v5
- with:
- context: .
- target: backend
- push: true
- tags: |
- ${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}-tina:${{ steps.tag.outputs.sha }}
- ${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}-tina:latest
-
- name: Update image tags in gitops repo
env:
CI_TOKEN: ${{ secrets.CI_TOKEN }}
@@ -63,10 +53,7 @@ jobs:
sed -i \
"s|image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}:.*|image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}:${SHA}|" \
"apps/${{ env.APP }}/deployment.yaml"
- sed -i \
- "s|image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}-tina:.*|image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ env.APP }}-tina:${SHA}|" \
- "apps/${{ env.APP }}/tina-backend-deployment.yaml"
- git add "apps/${{ env.APP }}/deployment.yaml" "apps/${{ env.APP }}/tina-backend-deployment.yaml"
+ git add "apps/${{ env.APP }}/deployment.yaml"
git diff --cached --quiet && echo "No changes, skipping commit." && exit 0
git commit -m "ci: update ${{ env.APP }} to ${SHA}"
git push origin main
diff --git a/sanity.config.ts b/sanity.config.ts
new file mode 100644
index 0000000..ef2d2b2
--- /dev/null
+++ b/sanity.config.ts
@@ -0,0 +1,30 @@
+import { defineConfig } from 'sanity'
+import { structureTool } from 'sanity/structure'
+import { visionTool } from '@sanity/vision'
+import { schemaTypes } from './src/sanity/schemas'
+
+export default defineConfig({
+ projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID,
+ dataset: import.meta.env.PUBLIC_SANITY_DATASET ?? 'production',
+ plugins: [
+ structureTool({
+ structure: (S) =>
+ S.list()
+ .title('Content')
+ .items([
+ S.listItem()
+ .title('Theater Homepage')
+ .child(
+ S.document()
+ .schemaType('theaterHomepage')
+ .documentId('theaterHomepage')
+ ),
+ S.divider(),
+ S.documentTypeListItem('post').title('Blog Posts'),
+ S.documentTypeListItem('page').title('Pages'),
+ ]),
+ }),
+ visionTool(),
+ ],
+ schema: { types: schemaTypes },
+})
diff --git a/scripts/migrate-to-sanity.mjs b/scripts/migrate-to-sanity.mjs
new file mode 100644
index 0000000..b6addf3
--- /dev/null
+++ b/scripts/migrate-to-sanity.mjs
@@ -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)
+}
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 0000000..da596b7
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/src/sanity/client.ts b/src/sanity/client.ts
new file mode 100644
index 0000000..decd8a7
--- /dev/null
+++ b/src/sanity/client.ts
@@ -0,0 +1 @@
+export { sanityClient } from 'sanity:client'
diff --git a/src/sanity/imageUrl.ts b/src/sanity/imageUrl.ts
new file mode 100644
index 0000000..7cb4d27
--- /dev/null
+++ b/src/sanity/imageUrl.ts
@@ -0,0 +1,11 @@
+import { createImageUrlBuilder } from '@sanity/image-url'
+
+const builder = createImageUrlBuilder({
+ projectId: import.meta.env.PUBLIC_SANITY_PROJECT_ID,
+ dataset: import.meta.env.PUBLIC_SANITY_DATASET ?? 'production',
+})
+
+export function urlFor(source: any): string {
+ if (!source) return ''
+ return builder.image(source).url()
+}
diff --git a/src/sanity/portableText.ts b/src/sanity/portableText.ts
new file mode 100644
index 0000000..b6d09fa
--- /dev/null
+++ b/src/sanity/portableText.ts
@@ -0,0 +1,6 @@
+import { toHTML } from '@portabletext/to-html'
+
+export function renderPortableText(blocks: any[]): string {
+ if (!blocks?.length) return ''
+ return toHTML(blocks)
+}
diff --git a/src/sanity/queries.ts b/src/sanity/queries.ts
new file mode 100644
index 0000000..15586ca
--- /dev/null
+++ b/src/sanity/queries.ts
@@ -0,0 +1,34 @@
+export const theaterHomepageQuery = `*[_type == "theaterHomepage" && _id == "theaterHomepage"][0]`
+
+export const allPostsQuery = `
+ *[_type == "post"] | order(date desc) {
+ _id,
+ title,
+ "slug": slug.current,
+ date,
+ excerpt,
+ coverImage,
+ body
+ }
+`
+
+export const postBySlugQuery = `
+ *[_type == "post" && slug.current == $slug][0] {
+ _id,
+ title,
+ "slug": slug.current,
+ date,
+ excerpt,
+ coverImage,
+ body
+ }
+`
+
+export const pageBySlugQuery = `
+ *[_type == "page" && slug.current == $slug][0] {
+ _id,
+ title,
+ description,
+ body
+ }
+`
diff --git a/src/sanity/schemas/index.ts b/src/sanity/schemas/index.ts
new file mode 100644
index 0000000..9d6ca6f
--- /dev/null
+++ b/src/sanity/schemas/index.ts
@@ -0,0 +1,5 @@
+import { postSchema } from './post'
+import { pageSchema } from './page'
+import { theaterHomepageSchema } from './theaterHomepage'
+
+export const schemaTypes = [postSchema, pageSchema, theaterHomepageSchema]
diff --git a/src/sanity/schemas/page.ts b/src/sanity/schemas/page.ts
new file mode 100644
index 0000000..f5c3188
--- /dev/null
+++ b/src/sanity/schemas/page.ts
@@ -0,0 +1,33 @@
+import { defineType, defineField } from 'sanity'
+
+export const pageSchema = defineType({
+ name: 'page',
+ title: 'Pages',
+ type: 'document',
+ fields: [
+ defineField({
+ name: 'title',
+ title: 'Title',
+ type: 'string',
+ validation: (r) => r.required(),
+ }),
+ defineField({
+ name: 'slug',
+ title: 'Slug',
+ type: 'slug',
+ options: { source: 'title' },
+ }),
+ defineField({
+ name: 'description',
+ title: 'Description',
+ type: 'string',
+ }),
+ defineField({
+ name: 'body',
+ title: 'Content',
+ type: 'array',
+ of: [{ type: 'block' }],
+ validation: (r) => r.required(),
+ }),
+ ],
+})
diff --git a/src/sanity/schemas/post.ts b/src/sanity/schemas/post.ts
new file mode 100644
index 0000000..b0da0c0
--- /dev/null
+++ b/src/sanity/schemas/post.ts
@@ -0,0 +1,47 @@
+import { defineType, defineField } from 'sanity'
+
+export const postSchema = defineType({
+ name: 'post',
+ title: 'Blog Posts',
+ type: 'document',
+ fields: [
+ defineField({
+ name: 'title',
+ title: 'Title',
+ type: 'string',
+ validation: (r) => r.required(),
+ }),
+ defineField({
+ name: 'slug',
+ title: 'Slug',
+ type: 'slug',
+ options: { source: 'title' },
+ validation: (r) => r.required(),
+ }),
+ defineField({
+ name: 'date',
+ title: 'Publication Date',
+ type: 'datetime',
+ validation: (r) => r.required(),
+ }),
+ defineField({
+ name: 'excerpt',
+ title: 'Excerpt',
+ type: 'string',
+ validation: (r) => r.required(),
+ }),
+ defineField({
+ name: 'coverImage',
+ title: 'Cover Image',
+ type: 'image',
+ options: { hotspot: true },
+ }),
+ defineField({
+ name: 'body',
+ title: 'Body',
+ type: 'array',
+ of: [{ type: 'block' }, { type: 'image' }],
+ validation: (r) => r.required(),
+ }),
+ ],
+})
diff --git a/src/sanity/schemas/theaterHomepage.ts b/src/sanity/schemas/theaterHomepage.ts
new file mode 100644
index 0000000..3a37c65
--- /dev/null
+++ b/src/sanity/schemas/theaterHomepage.ts
@@ -0,0 +1,173 @@
+import { defineType, defineField, defineArrayMember } from 'sanity'
+
+export const theaterHomepageSchema = defineType({
+ name: 'theaterHomepage',
+ title: 'Theater Homepage',
+ type: 'document',
+ // Singleton: disable create/delete — only update and publish
+ __experimental_actions: ['update', 'publish'],
+ fields: [
+ defineField({
+ name: 'title',
+ title: 'Page Title',
+ type: 'string',
+ }),
+
+ // Hero Section
+ defineField({
+ name: 'hero',
+ title: 'Hero Section',
+ type: 'object',
+ fields: [
+ defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'subtitle_de', title: 'Subtitle (German)', type: 'text' }),
+ defineField({ name: 'subtitle_en', title: 'Subtitle (English)', type: 'text' }),
+ ],
+ }),
+
+ // Section 1: Das Projekt / The Project
+ defineField({
+ name: 'projekt',
+ title: 'Section 1: Das Projekt / The Project',
+ type: 'object',
+ fields: [
+ defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
+ defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
+ ],
+ }),
+
+ // Section 2: Wer steckt dahinter / Who's Behind It
+ defineField({
+ name: 'team',
+ title: "Section 2: Wer steckt dahinter / Who's Behind It",
+ type: 'object',
+ fields: [
+ defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
+ defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
+ defineField({
+ name: 'organizations',
+ title: 'Organizations',
+ type: 'array',
+ of: [
+ defineArrayMember({
+ type: 'object',
+ fields: [
+ defineField({ name: 'name', title: 'Name', type: 'string' }),
+ defineField({ name: 'role_de', title: 'Role (German)', type: 'string' }),
+ defineField({ name: 'role_en', title: 'Role (English)', type: 'string' }),
+ defineField({ name: 'logo', title: 'Logo', type: 'image', options: { hotspot: false } }),
+ defineField({ name: 'url', title: 'Website URL', type: 'url' }),
+ ],
+ }),
+ ],
+ }),
+ ],
+ }),
+
+ // Section 3: Das Stück / The Play
+ defineField({
+ name: 'stueck',
+ title: 'Section 3: Das Stück / The Play',
+ type: 'object',
+ fields: [
+ defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
+ defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
+ ],
+ }),
+
+ // Section 4: Mitwirkung / Participation
+ defineField({
+ name: 'mitwirkung',
+ title: 'Section 4: Mitwirkung / Participation',
+ type: 'object',
+ fields: [
+ defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
+ defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
+ defineField({ name: 'director_name', title: 'Director Name', type: 'string' }),
+ defineField({ name: 'director_email', title: 'Director Email', type: 'string' }),
+ defineField({ name: 'director_phone', title: 'Director Phone', type: 'string' }),
+ ],
+ }),
+
+ // Section 5: Termine / Schedule
+ defineField({
+ name: 'termine',
+ title: 'Section 5: Termine / Schedule',
+ type: 'object',
+ fields: [
+ defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
+ defineField({
+ name: 'events',
+ title: 'Events',
+ type: 'array',
+ of: [
+ defineArrayMember({
+ type: 'object',
+ fields: [
+ defineField({ name: 'title_de', title: 'Event Title (German)', type: 'string' }),
+ defineField({ name: 'title_en', title: 'Event Title (English)', type: 'string' }),
+ defineField({ name: 'date', title: 'Date / Period', type: 'string' }),
+ defineField({ name: 'description_de', title: 'Description (German)', type: 'text' }),
+ defineField({ name: 'description_en', title: 'Description (English)', type: 'text' }),
+ ],
+ }),
+ ],
+ }),
+ ],
+ }),
+
+ // Section 6: Presse / Press
+ defineField({
+ name: 'presse',
+ title: 'Section 6: Presse / Press',
+ type: 'object',
+ fields: [
+ defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
+ defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
+ ],
+ }),
+
+ // Section 7: Foto Galerie / Photo Gallery
+ defineField({
+ name: 'gallery',
+ title: 'Section 7: Foto Galerie / Photo Gallery',
+ type: 'object',
+ fields: [
+ defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
+ defineField({
+ name: 'images',
+ title: 'Gallery Images',
+ type: 'array',
+ of: [{ type: 'image' }],
+ }),
+ ],
+ }),
+
+ // Section 8: Tickets
+ defineField({
+ name: 'tickets',
+ title: 'Section 8: Tickets',
+ type: 'object',
+ fields: [
+ defineField({ name: 'title_de', title: 'Title (German)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'title_en', title: 'Title (English)', type: 'string', validation: (r) => r.required() }),
+ defineField({ name: 'content_de', title: 'Content (German)', type: 'array', of: [{ type: 'block' }] }),
+ defineField({ name: 'content_en', title: 'Content (English)', type: 'array', of: [{ type: 'block' }] }),
+ defineField({ name: 'ticket_url', title: 'Ticket Purchase URL', type: 'url' }),
+ ],
+ }),
+ ],
+})