Shumiland/src/syncBlogFromStaging.ts
Vadym Samoilenko f2a560f9e6 feat: kvytky 1:1 redesign, CMS-driven pages, blog/group/birthday updates, green-wave pattern, image optimization
- /kvytky: combo grid (cart add), tabbed catalog with Figma chips/photos, CMS-editable (TicketsPage global)
- Tariffs collection: infoChips, badgeLabel, category zone/attraction/program, manual tickets
- Birthday & Group pages: Figma design + real photos, pricing sections removed (form-only), unified form style
- Thank-you page (pidtverdzhennya) from Figma; combo cards redesigned (4-up grid)
- Reviews coverflow slider; blog photos fixed (relative media URLs for next/image)
- JSON-LD (WebPage/LocalBusiness/CollectionPage/Service) on home/lokatsii/birthday/group
- Footer: photo bg + AImpress credit link; green wave pattern on green sections; consistent footer
- Imported 3 real blog posts from staging; fixed text artifacts
- Optimized 118 images 563MB -> 72MB (resize 1920px + re-encode)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 16:46:52 +01:00

128 lines
3.3 KiB
TypeScript

/* eslint-disable no-console */
import 'dotenv/config'
import { writeFile } from 'fs/promises'
import { getPayload } from 'payload'
import config from '../payload.config.js'
const STAGING = 'https://shumi.ai-impress.com'
// Seed placeholders to remove once real articles are imported.
const SEED_TITLES = [
'Сезон динозаврів відкрито!',
'Весняні канікули в Шуміленді',
'Нова локація: Тир з призами',
]
interface StagingPost {
title: string
slug: string
publishedAt?: string | null
excerpt?: string | null
body?: unknown
status?: string
hero?: { url?: string | null; alt?: string | null } | null
}
async function downloadHero(url: string, slug: string): Promise<string | null> {
try {
const res = await fetch(url)
if (!res.ok) return null
const buf = Buffer.from(await res.arrayBuffer())
const ct = res.headers.get('content-type') ?? ''
const ext = ct.includes('png')
? 'png'
: ct.includes('jpeg')
? 'jpg'
: ct.includes('webp')
? 'webp'
: 'bin'
const path = `/tmp/blog-${slug}.${ext}`
await writeFile(path, buf)
return path
} catch {
return null
}
}
async function run(): Promise<void> {
const payload = await getPayload({ config })
const res = await fetch(
`${STAGING}/api/blog-posts?limit=50&depth=1&where[status][equals]=published`
)
const data = (await res.json()) as { docs: StagingPost[] }
const posts = data.docs ?? []
console.log(`Staging published posts: ${posts.length}`)
let created = 0
let skipped = 0
for (const p of posts) {
const existing = await payload.find({
collection: 'blog-posts',
where: { title: { equals: p.title } },
limit: 1,
overrideAccess: true,
})
if (existing.totalDocs > 0) {
console.log(`= exists: ${p.title}`)
skipped++
continue
}
let heroId: number | undefined
const heroUrl = p.hero?.url
if (heroUrl) {
const full = heroUrl.startsWith('http') ? heroUrl : `${STAGING}${heroUrl}`
const filePath = await downloadHero(full, p.slug)
if (filePath) {
const media = await payload.create({
collection: 'media',
filePath,
data: { alt: p.hero?.alt ?? p.title },
overrideAccess: true,
})
heroId = media.id as number
}
}
await payload.create({
collection: 'blog-posts',
data: {
title: p.title,
publishedAt: p.publishedAt ?? undefined,
excerpt: p.excerpt ?? undefined,
body: (p.body ?? undefined) as any,
status: 'published',
...(heroId ? { hero: heroId } : {}),
},
overrideAccess: true,
})
console.log(`+ imported: ${p.title}${heroId ? ' (with hero)' : ''}`)
created++
}
// Remove seed placeholders
let deleted = 0
for (const title of SEED_TITLES) {
const r = await payload.delete({
collection: 'blog-posts',
where: { title: { equals: title } },
overrideAccess: true,
})
const count = Array.isArray(r.docs) ? r.docs.length : 0
if (count > 0) {
console.log(`- removed seed: ${title}`)
deleted += count
}
}
console.log(`\nDone. Imported: ${created}, skipped: ${skipped}, seed removed: ${deleted}`)
process.exit(0)
}
run().catch((err) => {
console.error(err)
process.exit(1)
})