fix(about): wrap bio richText in collapsible so Lexical editor renders in admin
Some checks failed
ci/woodpecker/push/web Pipeline failed

The bare richText field was silently not rendering in the Payload admin UI.
Wrapping in a collapsible preserves the data path while giving the editor
a proper container. Also renames image assets and adds media management scripts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-15 21:11:10 -05:00
parent b47c5e420a
commit 936a98f955
13 changed files with 261 additions and 1 deletions

179
scripts/update-images.ts Normal file
View File

@@ -0,0 +1,179 @@
/**
* Update images — jasonwoltje.com
* Uploads new photos and assigns them to globals/posts.
* Idempotent by alt-text match.
*/
import { getPayload } from 'payload'
import config from '@payload-config'
import fs from 'node:fs'
import path from 'node:path'
async function main() {
const payload = await getPayload({ config })
const imagesDir = path.resolve(process.cwd(), 'images')
const uploads: Array<{ file: string; alt: string; key: string }> = [
{
file: 'gpt-image-1.5_creative_tech_founder_portrait_man_with_bald_head_and_brown-ginger_full_beard_bl-0.jpg',
alt: 'Jason Woltje — tech founder portrait, dark background',
key: 'hero',
},
{
file: 'gpt-image-1.5_bold_social_media_profile_portrait_man_with_bald_head_and_brown-ginger_full_bear-0.jpg',
alt: 'Jason Woltje — social media profile, neon gradient',
key: 'og',
},
{
file: 'gpt-image-1.5_documentary_style_environmental_portrait_man_with_bald_head_and_brown-ginger_ful-0.jpg',
alt: 'Jason Woltje — at the desk, documentary style',
key: 'desk',
},
{
file: 'gpt-image-1.5_illustrated_portrait_stylized_modern_digital_art_style_man_with_bald_head_and_br-0.jpg',
alt: 'Jason Woltje — illustrated portrait',
key: 'illustration',
},
{
file: 'gpt-image-1.5_authoritative_thought_leader_portrait_man_with_bald_head_and_brown-ginger_full_b-0.jpg',
alt: 'Jason Woltje — thought leader portrait, city backdrop',
key: 'thought-leader',
},
{
file: 'gpt-image-1.5_high-end_fashion_forward_portrait_man_with_bald_head_and_brown-ginger_full_beard-0.jpg',
alt: 'Jason Woltje — fashion editorial portrait',
key: 'editorial',
},
]
const ids: Record<string, number> = {}
console.log('\n── Uploading new images ──────────────────────────────────')
for (const u of uploads) {
const filePath = path.join(imagesDir, u.file)
if (!fs.existsSync(filePath)) {
console.log(` ⚠ Missing: ${u.file}`)
continue
}
const existing = await payload.find({
collection: 'media',
where: { alt: { equals: u.alt } },
limit: 1,
})
if (existing.totalDocs > 0) {
ids[u.key] = existing.docs[0]!.id as number
console.log(` ↷ Already exists: ${u.alt} (id=${ids[u.key]})`)
continue
}
// Update alt of old upload if it used a generic name (from initial seed)
const doc = await payload.create({
collection: 'media',
filePath,
data: { alt: u.alt },
})
ids[u.key] = doc.id as number
console.log(` ✓ Uploaded: ${u.alt} (id=${doc.id})`)
}
// ── Update Home hero ──────────────────────────────────────────────────
if (ids['hero']) {
console.log('\n── Updating Home hero image ─────────────────────────────')
const home = await payload.findGlobal({ slug: 'home', depth: 0 })
const heroData = (home as Record<string, unknown>).hero as Record<string, unknown> | undefined
await payload.updateGlobal({
slug: 'home',
data: {
hero: {
...(heroData ?? {}),
heroImage: ids['hero'],
},
},
})
console.log(` ✓ Home heroImage → id=${ids['hero']}`)
}
// ── Update SEO OG image ───────────────────────────────────────────────
if (ids['og']) {
console.log('\n── Updating SEO defaultOgImage ──────────────────────────')
await payload.updateGlobal({
slug: 'seo',
data: { defaultOgImage: ids['og'] },
})
console.log(` ✓ SEO defaultOgImage → id=${ids['og']}`)
}
// ── Update post cover images ──────────────────────────────────────────
console.log('\n── Updating post cover images ───────────────────────────')
const postCovers: Array<{ slugContains: string; imageKey: string }> = [
{ slugContains: 'cascading-traefik', imageKey: 'desk' },
{ slugContains: 'migrating-from-it', imageKey: 'thought-leader' },
{ slugContains: 'payload-3', imageKey: 'illustration' },
]
for (const pc of postCovers) {
const imageId = ids[pc.imageKey]
if (!imageId) continue
const { docs } = await payload.find({
collection: 'posts',
where: { slug: { contains: pc.slugContains } },
limit: 1,
depth: 0,
})
if (docs.length === 0) {
console.log(` ⚠ Post not found: slug contains "${pc.slugContains}"`)
continue
}
const post = docs[0]!
await payload.update({
collection: 'posts',
id: post.id,
data: { coverImage: imageId },
})
console.log(` ✓ Post "${post.slug}" coverImage → id=${imageId}`)
}
// ── Assign project hero images ────────────────────────────────────────
console.log('\n── Updating project hero images ─────────────────────────')
const projectHeroes: Array<{ slugContains: string; imageKey: string }> = [
{ slugContains: 'mosaic-stack', imageKey: 'hero' },
{ slugContains: 'jasonwoltje', imageKey: 'editorial' },
]
for (const ph of projectHeroes) {
const imageId = ids[ph.imageKey]
if (!imageId) continue
const { docs } = await payload.find({
collection: 'projects',
where: { slug: { contains: ph.slugContains } },
limit: 1,
depth: 0,
})
if (docs.length === 0) {
console.log(` ⚠ Project not found: slug contains "${ph.slugContains}"`)
continue
}
const project = docs[0]!
await payload.update({
collection: 'projects',
id: project.id,
data: { heroImage: imageId },
})
console.log(` ✓ Project "${project.slug}" heroImage → id=${imageId}`)
}
console.log('\n════════════════════════════════════════════════════════')
console.log(' IMAGE UPDATE COMPLETE')
console.log('════════════════════════════════════════════════════════\n')
process.exit(0)
}
main().catch((err) => {
console.error('Fatal:', err)
process.exit(1)
})