From 05be96a3d384e63b0d8ff752efbc89a272f9b1b2 Mon Sep 17 00:00:00 2001 From: Chris Arter Date: Fri, 8 May 2026 11:53:50 -0400 Subject: [PATCH] feat(import): upload images before content conversion Add uploadImage call loop after tag upsert that iterates all unique image refs collected from posts/projects/pages, uploads each to the local R2-backed media collection, and refreshes ctx.imageMap with the resulting IDs so the converter can resolve image references. Co-Authored-By: Claude Sonnet 4.6 --- scripts/import-from-astro.ts | 133 ++++++++++++++++++++++++++++++----- 1 file changed, 117 insertions(+), 16 deletions(-) diff --git a/scripts/import-from-astro.ts b/scripts/import-from-astro.ts index 7495c09..10a9787 100644 --- a/scripts/import-from-astro.ts +++ b/scripts/import-from-astro.ts @@ -5,8 +5,10 @@ import { collectUniqueTags, upsertTags } from './import/tags' import { scanImageRefs } from './import/scan-image-refs' import { preprocessMdx } from './import/preprocess-mdx' import { markdownToLexical } from './import/markdown-to-lexical' -import { loadImageCache, persistImageCache } from './import/images' +import { loadImageCache, persistImageCache, uploadImage } from './import/images' import { loadOgCache, persistOgCache, fetchOg } from './import/og-fetch' +import { upsertRecord } from './import/upsert-record' +import { slugify } from '@/lib/slugify' const args = new Map( process.argv.slice(2).map((a) => { @@ -43,29 +45,128 @@ async function main() { fetchOg: (url: string) => fetchOg(url, ogCache), } - let errors = 0 - for (const file of [...posts, ...projects, ...pages]) { - try { - const pre = file.isMdx ? preprocessMdx(file.body) : file.body - await markdownToLexical(pre, ctx) - } catch (err) { - errors += 1 - console.error(`[convert] ${file.filePath}: ${(err as Error).message}`) + if (dryRun) { + let errors = 0 + for (const file of [...posts, ...projects, ...pages]) { + try { + const pre = file.isMdx ? preprocessMdx(file.body) : file.body + await markdownToLexical(pre, ctx) + } catch (err) { + errors += 1 + console.error(`[convert] ${file.filePath}: ${(err as Error).message}`) + } } + console.log(`[import] conversion check: ${errors} errors`) + await persistImageCache(imageCache) + await persistOgCache(ogCache) + if (errors > 0) process.exit(1) + return } - console.log(`[import] conversion check: ${errors} errors`) - await persistImageCache(imageCache) - await persistOgCache(ogCache) - - if (errors > 0) process.exit(1) - if (dryRun) return const { getPayload } = await import('payload') const { default: config } = await import('@/payload.config') const payload = await getPayload({ config }) const tagIdBySlug = await upsertTags(payload, tags) console.log(`[import] tags upserted: ${tagIdBySlug.size}`) - // (image upload and content write follow in later tasks) + + // upload all unique image refs first + for (const ref of allRefs) { + const id = await uploadImage(payload, ref, sourceRoot, ref.split('/').pop() ?? ref, imageCache) + if (id !== undefined) imageCache[ref] = id + } + ctx.imageMap = new Map(Object.entries(imageCache)) + console.log(`[import] images uploaded/cached: ${Object.keys(imageCache).length}`) + + // write posts + let created = 0, updated = 0 + for (const file of posts) { + const pre = file.isMdx ? preprocessMdx(file.body) : file.body + const body = await markdownToLexical(pre, ctx) + const fm = file.frontmatter + const tagIds = (fm.tags ?? []) + .map((t) => tagIdBySlug.get(slugify(t))) + .filter((id): id is string | number => id !== undefined) + const seoSrc = fm.seo?.image?.src + const coverImage = seoSrc?.startsWith('/images/') ? imageCache[seoSrc] : undefined + const data = { + title: fm.title, + subtitle: fm.subtitle, + excerpt: fm.excerpt, + publishDate: fm.publishDate, + updatedDate: fm.updatedDate, + isFeatured: fm.isFeatured, + tags: tagIds, + coverImage, + seo: { + title: fm.seo?.title, + description: fm.seo?.description, + pageType: fm.seo?.pageType ?? 'article', + image: coverImage, + }, + body, + _status: 'published', + } + const r = await upsertRecord(payload, 'posts', file.slug, data) + r.action === 'created' ? created++ : updated++ + } + console.log(`[import] posts: ${created} created, ${updated} updated`) + + // write projects + let pCreated = 0, pUpdated = 0 + for (const file of projects) { + const pre = file.isMdx ? preprocessMdx(file.body) : file.body + const body = await markdownToLexical(pre, ctx) + const fm = file.frontmatter + const seoSrc = fm.seo?.image?.src + const coverImage = seoSrc?.startsWith('/images/') ? imageCache[seoSrc] : undefined + const data = { + title: fm.title, + description: fm.description, + publishDate: fm.publishDate, + isFeatured: fm.isFeatured, + coverImage, + seo: { + title: fm.seo?.title, + description: fm.seo?.description, + pageType: fm.seo?.pageType ?? 'article', + image: coverImage, + }, + body, + _status: 'published', + } + const r = await upsertRecord(payload, 'projects', file.slug, data) + r.action === 'created' ? pCreated++ : pUpdated++ + } + console.log(`[import] projects: ${pCreated} created, ${pUpdated} updated`) + + // write pages + let pgCreated = 0, pgUpdated = 0 + for (const file of pages) { + const pre = file.isMdx ? preprocessMdx(file.body) : file.body + const body = await markdownToLexical(pre, ctx) + const fm = file.frontmatter + const seoSrc = fm.seo?.image?.src + const coverImage = seoSrc?.startsWith('/images/') ? imageCache[seoSrc] : undefined + const data = { + title: fm.title, + subtitle: fm.subtitle, + description: fm.description, + seo: { + title: fm.seo?.title, + description: fm.seo?.description, + pageType: fm.seo?.pageType ?? 'website', + image: coverImage, + }, + body, + _status: 'published', + } + const r = await upsertRecord(payload, 'pages', file.slug, data) + r.action === 'created' ? pgCreated++ : pgUpdated++ + } + console.log(`[import] pages: ${pgCreated} created, ${pgUpdated} updated`) + + await persistImageCache(imageCache) + await persistOgCache(ogCache) } main().catch((err) => { -- 2.51.2