/** * One-shot content hygiene from dogfood QA (grammar, Flickr residue, junk tags, etc.) * * npx tsx scripts/content-cleanup.ts */ import { prisma } from "../src/lib/prisma.js"; import { reindexAllProjects } from "../src/lib/typesense.js"; function cleanDescription(text: string): string { let t = text || ""; t = t.replace(/\bcompanies sensors\b/gi, "company's sensors"); t = t.replace(/\b3d model\b/g, "3D model"); t = t.replace(/\b3d models\b/g, "3D models"); t = t.replace(/\bgaurded\b/gi, "guarded"); t = t.replace(/\bView\s+On\s+Black\b/gi, ""); t = t.replace(/[ \t]+\n/g, "\n"); t = t.replace(/\n{3,}/g, "\n\n"); return t.trim(); } function makeShort(description: string, existing: string | null): string | null { const cleaned = cleanDescription(existing || description || ""); if (!cleaned) return null; // Prefer first sentence if reasonably short const sentence = cleaned.match(/^[^.!?]+[.!?]/)?.[0]; if (sentence && sentence.length >= 40 && sentence.length <= 180) return sentence.trim(); if (cleaned.length <= 180) return cleaned; return cleaned.slice(0, 177).replace(/\s+\S*$/, "") + "…"; } function isJunkTag(name: string): boolean { const t = name.trim(); if (!t) return true; if (/^\d+$/.test(t)) return true; if (/^\d+mm$/i.test(t)) return true; if (/nikon|canon|nikkor|sony|sigma|tamron|exif|\bd90\b|\bd700|\bd800|e5000/i.test(t)) return true; if (/^after$/i.test(t) || /^effects$/i.test(t)) return true; if (/viewonblack|view.?on.?black/i.test(t)) return true; if (t.length >= 14 && !/\s/.test(t) && /[0-9]/.test(t)) return true; if (/369rollinghill|frenchcreekstatepark/i.test(t)) return true; return false; } const DESCRIPTION_OVERRIDES: Record = { ankle: "3D animated anatomical study of a human ankle, modeled and rendered for motion work and produced with Modo and After Effects.", "agi-engineering-v-process-diagram": "Motion graphics process diagram explaining AGI’s Engineering V methodology for industrial automation projects—used in technical and marketing communications.", "wired-for-sound-animated-tv-spot": "Animated television spot for Wired for Sound, combining motion graphics and 2D animation for broadcast.", "aikido-kokikai-federation-akf-logo": "Video identity sequence for the Aikido Kokikai Federation (AKF) logo—brand motion for federation communications.", }; async function main() { console.log("Content cleanup starting…"); const projects = await prisma.project.findMany({ select: { id: true, slug: true, title: true, description: true, shortDescription: true, }, }); let textFixed = 0; for (const p of projects) { let description = cleanDescription(p.description || ""); let shortDescription = p.shortDescription ? cleanDescription(p.shortDescription) : null; if (DESCRIPTION_OVERRIDES[p.slug]) { description = DESCRIPTION_OVERRIDES[p.slug]; } // Eagle: shortDescription was truncated mid-word if (p.slug === "eagle-in-flight" && shortDescription && /tru$/.test(shortDescription)) { shortDescription = makeShort(description, null); } // When short is empty, a full dump, or identical to full desc — derive a blurb if ( !shortDescription || shortDescription === description || (description && shortDescription.length > 200 && shortDescription.startsWith(description.slice(0, 40))) ) { shortDescription = makeShort(description, null); } if ( description !== (p.description || "") || shortDescription !== (p.shortDescription || null) ) { await prisma.project.update({ where: { id: p.id }, data: { description, shortDescription }, }); textFixed++; console.log(` text: ${p.slug}`); } } console.log(`Updated text on ${textFixed} projects`); // Rename CAD view to avoid /portfolio/cad collision with empty CAD category const cadView = await prisma.portfolioView.findFirst({ where: { slug: "cad" } }); if (cadView) { await prisma.portfolioView.update({ where: { id: cadView.id }, data: { slug: "technical-cad", name: "CAD & Technical" }, }); console.log("Renamed portfolio view cad → technical-cad"); } // Feature a few strong recent published projects if none featured const featuredCount = await prisma.project.count({ where: { featured: true, visibility: "published" }, }); if (featuredCount === 0) { const candidates = await prisma.project.findMany({ where: { visibility: "published" }, orderBy: [{ date: { sort: "desc", nulls: "last" } }, { displayPriority: "asc" }], take: 6, select: { id: true, title: true, slug: true }, }); for (const c of candidates) { await prisma.project.update({ where: { id: c.id }, data: { featured: true } }); console.log(` featured: ${c.slug}`); } console.log(`Featured ${candidates.length} projects`); } else { console.log(`Leaving ${featuredCount} existing featured projects`); } // Junk tags const tags = await prisma.tag.findMany({ select: { id: true, name: true, slug: true } }); let tagsRemoved = 0; for (const tag of tags) { if (!isJunkTag(tag.name) && !isJunkTag(tag.slug)) continue; await prisma.projectTag.deleteMany({ where: { tagId: tag.id } }); await prisma.tag.delete({ where: { id: tag.id } }); tagsRemoved++; console.log(` removed tag: ${tag.name}`); } console.log(`Removed ${tagsRemoved} junk tags`); // Merge After Effects if both tags somehow remain (handled by delete) try { const n = await reindexAllProjects(); console.log(`Typesense reindexed (${n} projects)`); } catch (err) { console.warn("Typesense reindex failed:", err); } console.log("Content cleanup done."); await prisma.$disconnect(); } main().catch(async (err) => { console.error(err); await prisma.$disconnect(); process.exit(1); });