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
This commit is contained in:
2026-08-04 16:14:24 -04:00
parent ba5325196c
commit a2cec1baf1
18 changed files with 479 additions and 42 deletions
+167
View File
@@ -0,0 +1,167 @@
/**
* 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);
});