Files
jmartgraphix.com/server/scripts/content-cleanup.ts
T
jmartin a2cec1baf1 Fix dogfood QA: 404, content hygiene, grid UX, a11y, CTAs
- Add branded NotFound catch-all (no more blank unknown routes)
- Portfolio cards: shimmer skeleton, eager first-row thumbs, real alts
- Hide empty categories; rename CAD view slug to avoid collision
- Resume summary preserves paragraphs/bullets
- Home/About/footer hire contact mailto CTA
- Sticky header scroll-padding; chip aria-labels
- Security headers; Flickr import strip View On Black + junk tags
- Content cleanup script for grammar, residue, tags, featured
2026-08-04 16:14:24 -04:00

168 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<string, string> = {
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 AGIs 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);
});