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
+3
View File
@@ -72,6 +72,9 @@ export function Layout() {
</div>
</div>
<div className="site-footer__links">
<a href={`mailto:${site?.social?.email || "jmartin@jmartgraphix.com"}`}>
Contact
</a>
{site?.social?.youtube && (
<a href={site.social.youtube} target="_blank" rel="noreferrer">
YouTube
+33 -1
View File
@@ -27,11 +27,34 @@
overflow: hidden;
background: #0e0e10;
&.is-loading::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(
110deg,
#141416 0%,
#1c1c20 40%,
#141416 60%,
#0e0e10 100%
);
background-size: 200% 100%;
animation: project-card-shimmer 1.25s ease-in-out infinite;
z-index: 0;
}
img {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.55s ease;
opacity: 0;
transition: transform 0.55s ease, opacity 0.35s ease;
&.is-loaded {
opacity: 1;
}
}
}
@@ -93,3 +116,12 @@
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.35rem;
}
@keyframes project-card-shimmer {
0% {
background-position: 100% 0;
}
100% {
background-position: -100% 0;
}
}
+17 -6
View File
@@ -1,12 +1,17 @@
import { useState } from "react";
import { Link } from "react-router-dom";
import type { Project } from "../lib/api";
import { thumbOf } from "../lib/api";
import { thumbOf, thumbAltOf } from "../lib/api";
import { formatProjectYear } from "../lib/dates";
import "./ProjectCard.scss";
export function ProjectCard({ project, index = 0 }: { project: Project; index?: number }) {
const thumb = thumbOf(project);
const alt = thumbAltOf(project);
const year = formatProjectYear(project.date);
const [loaded, setLoaded] = useState(false);
// First row-ish: eager load so portfolio doesn't look empty pre-scroll
const eager = index < 8;
return (
<article
@@ -14,9 +19,17 @@ export function ProjectCard({ project, index = 0 }: { project: Project; index?:
style={{ animationDelay: `${Math.min(index, 12) * 40}ms` }}
>
<Link to={`/project/${project.slug}`} className="project-card__link">
<div className="project-card__media">
<div className={`project-card__media${thumb && !loaded ? " is-loading" : ""}`}>
{thumb ? (
<img src={thumb} alt="" loading="lazy" />
<img
src={thumb}
alt={alt}
loading={eager ? "eager" : "lazy"}
decoding="async"
fetchPriority={eager ? "high" : "auto"}
onLoad={() => setLoaded(true)}
className={loaded ? "is-loaded" : undefined}
/>
) : (
<div className="project-card__placeholder" aria-hidden>
{project.title.slice(0, 1)}
@@ -32,9 +45,7 @@ export function ProjectCard({ project, index = 0 }: { project: Project; index?:
{year && <span className="project-card__year">{year}</span>}
</div>
<h3>{project.title}</h3>
{project.shortDescription && (
<p>{project.shortDescription}</p>
)}
{project.shortDescription && <p>{project.shortDescription}</p>}
</div>
</Link>
</article>