perf(seo): image sizes, semantic HTML, X-Robots-Tag headers

- Add `sizes` prop to 8 <Image> components across news, heritage, and
  application pages — tells the browser which srcset variant to download,
  improving LCP and reducing bandwidth
- Replace date <span> with <time dateTime={ISO}> on news pages —
  Google uses datetime for article freshness signals
- Wrap news cards and article content in <article> tags — semantic
  boundary for crawlers
- Add X-Robots-Tag: noindex, nofollow header to all /hq-command
  responses in proxy.ts — defense-in-depth alongside meta robots

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 18:04:40 -05:00
parent 6b9a94490b
commit 8d80cbbc27
5 changed files with 25 additions and 17 deletions
@@ -904,7 +904,7 @@ function ExpandedCaseStudy({ node }: { node: any }) {
const fullImgSrc = `/cases/${nodeSlug}/${img}`;
return (
<div key={`g-${idx}`} className="relative w-full aspect-video rounded-2xl md:rounded-3xl overflow-hidden border border-black/10 dark:border-white/10 shadow-lg group bg-white">
<Image src={fullImgSrc} alt="Installation" fill className="object-cover group-hover:scale-105 transition-transform duration-700" />
<Image src={fullImgSrc} alt="Installation" fill sizes="(max-width: 768px) 100vw, 50vw" className="object-cover group-hover:scale-105 transition-transform duration-700" />
<button
onClick={() => openLightbox(fullImgSrc)}
className="absolute inset-0 w-full h-full bg-black/0 hover:bg-black/30 active:bg-black/40 transition-all flex items-center justify-center opacity-0 group-hover:opacity-100 backdrop-blur-[0px] hover:backdrop-blur-sm touch-manipulation"
@@ -1045,7 +1045,7 @@ export default function ApplicationClient({ data, realCases, images }: { data: a
<section className="relative w-full h-[50vh] md:h-[70vh] flex items-end justify-start overflow-hidden">
{heroImage ? (
<Image src={heroImage} alt={data.title} fill className="object-cover object-center scale-105 animate-slow-zoom" priority />
<Image src={heroImage} alt={data.title} fill sizes="100vw" className="object-cover object-center scale-105 animate-slow-zoom" priority />
) : (
<div className="absolute inset-0 bg-gradient-to-br from-[#0066CC]/20 to-black/80" />
)}
@@ -1149,7 +1149,7 @@ export default function ApplicationClient({ data, realCases, images }: { data: a
<div className="flex items-center gap-5 flex-1">
<div className="w-16 h-16 md:w-20 md:h-20 rounded-2xl md:rounded-3xl bg-black/5 dark:bg-black/50 border border-black/10 dark:border-white/5 flex items-center justify-center shrink-0 overflow-hidden relative shadow-inner">
{node.mediaFileName ? (
<Image src={`/cases/${nodeToSlug(node.title)}/${node.mediaFileName}`} alt={node.title} fill className="object-cover opacity-90 group-hover:scale-110 transition-transform duration-700" />
<Image src={`/cases/${nodeToSlug(node.title)}/${node.mediaFileName}`} alt={node.title} fill sizes="100px" className="object-cover opacity-90 group-hover:scale-110 transition-transform duration-700" />
) : (
<Cpu size={28} className="text-[#0066CC]/50 dark:text-[#00F0FF]/50" />
)}
+1 -1
View File
@@ -256,7 +256,7 @@ export default async function HeritagePage({ params }: { params: Promise<{ local
{sec.type === 'image' && sec.mediaUrl && (
<div className="relative w-full h-80 md:h-[500px] rounded-3xl overflow-hidden border border-black/10 dark:border-white/10 shadow-2xl">
<Image src={`/heritage/${sec.mediaUrl}`} alt={sec.title || "Heritage Image"} fill className="object-cover grayscale hover:grayscale-0 transition-all duration-1000" />
<Image src={`/heritage/${sec.mediaUrl}`} alt={sec.title || "Heritage Image"} fill sizes="100vw" className="object-cover grayscale hover:grayscale-0 transition-all duration-1000" />
</div>
)}
+5 -5
View File
@@ -294,14 +294,14 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
<section className="relative w-full h-[50vh] md:h-[70vh] flex items-end justify-center overflow-hidden bg-[#1D1D1F]">
{article.coverImage && (
<Image src={`/news/${slug}/${article.coverImage}`} alt={article.title} fill className="object-cover object-center opacity-60" priority />
<Image src={`/news/${slug}/${article.coverImage}`} alt={article.title} fill sizes="100vw" className="object-cover object-center opacity-60" priority />
)}
<div className="absolute inset-0 bg-gradient-to-t from-white via-white/80 dark:from-[#0A0A0C] dark:via-[#0A0A0C]/80 to-transparent" />
<div className="relative z-10 max-w-4xl w-full mx-auto px-6 pb-12 md:pb-20 text-center">
<div className="flex flex-wrap items-center justify-center gap-4 text-xs font-semibold uppercase tracking-widest text-[#0066CC] dark:text-[#4DA6FF] mb-6">
<span className="flex items-center gap-1.5 bg-[#0066CC]/10 dark:bg-[#4DA6FF]/10 px-3 py-1.5 rounded-full"><Tag size={14}/> {article.category}</span>
<span className="flex items-center gap-1.5 text-[#86868B]"><Calendar size={14}/> {new Date(article.publishedAt).toLocaleDateString(locale === 'en' ? 'en-US' : locale, { month: 'long', day: 'numeric', year: 'numeric' })}</span>
<time dateTime={new Date(article.publishedAt).toISOString()} className="flex items-center gap-1.5 text-[#86868B]"><Calendar size={14}/> {new Date(article.publishedAt).toLocaleDateString(locale === 'en' ? 'en-US' : locale, { month: 'long', day: 'numeric', year: 'numeric' })}</time>
</div>
<h1 className="text-4xl md:text-6xl font-medium text-[#1D1D1F] dark:text-[#F5F5F7] tracking-tight mb-6 leading-tight">
{article.title}
@@ -312,7 +312,7 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
</div>
</section>
<div className="relative z-10 max-w-3xl mx-auto px-6 mt-12 md:mt-20">
<article className="relative z-10 max-w-3xl mx-auto px-6 mt-12 md:mt-20">
{/* 🔥 EL MARKDOWN TRADUCIDO AL VUELO 🔥 */}
<div className="max-w-none mb-16">
@@ -325,7 +325,7 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{gallery.map((imgSrc: string, idx: number) => (
<div key={idx} className={`relative rounded-3xl overflow-hidden bg-[#1D1D1F] border border-black/10 dark:border-white/10 ${idx === 0 && gallery.length % 2 !== 0 ? 'sm:col-span-2 h-64 md:h-96' : 'h-48 md:h-64'}`}>
<Image src={`/news/${slug}/gallery/${imgSrc}`} alt={`Gallery image ${idx + 1}`} fill className="object-cover hover:scale-105 transition-transform duration-700" />
<Image src={`/news/${slug}/gallery/${imgSrc}`} alt={`Gallery image ${idx + 1}`} fill sizes="(max-width: 640px) 100vw, 50vw" className="object-cover hover:scale-105 transition-transform duration-700" />
</div>
))}
</div>
@@ -352,7 +352,7 @@ export default async function ArticlePage({ params }: { params: Promise<{ slug:
</div>
)}
</div>
</div>
</article>
</main>
);
}
+9 -5
View File
@@ -76,10 +76,11 @@ export default async function NewsHub({ params }: { params: Promise<{ locale: st
{/* HERO ARTICLE */}
{heroArticle && (
<article>
<Link href={`/${locale}/news/${heroArticle.slug}`} className="group relative w-full rounded-[2rem] overflow-hidden bg-white/60 dark:bg-[#1D1D1F]/40 backdrop-blur-2xl border border-black/5 dark:border-white/10 flex flex-col md:flex-row shadow-lg hover:shadow-xl transition-all duration-500 hover:-translate-y-1">
<div className="w-full md:w-1/2 h-56 md:h-[400px] relative overflow-hidden bg-[#1D1D1F]">
{heroArticle.coverImage ? (
<Image src={`/news/${heroArticle.slug}/${heroArticle.coverImage}`} alt={heroArticle.title} fill className="object-cover transition-transform duration-700 group-hover:scale-105" />
<Image src={`/news/${heroArticle.slug}/${heroArticle.coverImage}`} alt={heroArticle.title} fill sizes="(max-width: 768px) 100vw, 50vw" className="object-cover transition-transform duration-700 group-hover:scale-105" />
) : (
<div className="absolute inset-0 bg-gradient-to-br from-[#0066CC]/20 to-[#0A0A0C] flex items-center justify-center"><Newspaper size={64} className="text-white/10" /></div>
)}
@@ -87,7 +88,7 @@ export default async function NewsHub({ params }: { params: Promise<{ locale: st
<div className="w-full md:w-1/2 p-6 md:p-10 flex flex-col justify-center">
<div className="flex items-center gap-3 mb-4">
<span className="text-[9px] font-bold uppercase tracking-widest text-[#0066CC] dark:text-[#4DA6FF] bg-[#0066CC]/10 dark:bg-[#4DA6FF]/10 px-2.5 py-1 rounded-full">{heroArticle.category}</span>
<span className="text-[10px] font-medium text-[#86868B] flex items-center gap-1"><Calendar size={12}/> {new Date(heroArticle.publishedAt).toLocaleDateString(locale === 'en' ? 'en-US' : locale, { month: 'short', day: 'numeric', year: 'numeric' })}</span>
<time dateTime={new Date(heroArticle.publishedAt).toISOString()} className="text-[10px] font-medium text-[#86868B] flex items-center gap-1"><Calendar size={12}/> {new Date(heroArticle.publishedAt).toLocaleDateString(locale === 'en' ? 'en-US' : locale, { month: 'short', day: 'numeric', year: 'numeric' })}</time>
</div>
<h2 className="text-2xl md:text-3xl font-medium text-[#1D1D1F] dark:text-white mb-3 leading-tight group-hover:text-[#0066CC] dark:group-hover:text-[#4DA6FF] transition-colors">{heroArticle.title}</h2>
<p className="text-sm text-[#86868B] dark:text-[#A1A1A6] leading-relaxed mb-6 line-clamp-3">{heroArticle.excerpt}</p>
@@ -96,16 +97,18 @@ export default async function NewsHub({ params }: { params: Promise<{ locale: st
</span>
</div>
</Link>
</article>
)}
{/* GRID COLUMNAS */}
{gridArticles.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6 mt-4">
{gridArticles.map((article) => (
<Link key={article.id} href={`/${locale}/news/${article.slug}`} className="group flex flex-col bg-white/60 dark:bg-[#1D1D1F]/40 backdrop-blur-md border border-black/5 dark:border-white/10 rounded-3xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-500 hover:-translate-y-1">
<article key={article.id} className="flex flex-col">
<Link href={`/${locale}/news/${article.slug}`} className="group flex flex-col flex-1 bg-white/60 dark:bg-[#1D1D1F]/40 backdrop-blur-md border border-black/5 dark:border-white/10 rounded-3xl overflow-hidden shadow-sm hover:shadow-md transition-all duration-500 hover:-translate-y-1">
<div className="w-full h-40 relative overflow-hidden bg-[#1D1D1F]">
{article.coverImage ? (
<Image src={`/news/${article.slug}/${article.coverImage}`} alt={article.title} fill className="object-cover transition-transform duration-700 group-hover:scale-105" />
<Image src={`/news/${article.slug}/${article.coverImage}`} alt={article.title} fill sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 25vw" className="object-cover transition-transform duration-700 group-hover:scale-105" />
) : (
<div className="absolute inset-0 bg-gradient-to-br from-[#0066CC]/10 to-[#0A0A0C]" />
)}
@@ -113,7 +116,7 @@ export default async function NewsHub({ params }: { params: Promise<{ locale: st
<div className="p-5 flex flex-col flex-1">
<div className="flex justify-between items-center mb-3">
<span className="text-[8px] font-bold uppercase tracking-widest text-[#0066CC] dark:text-[#4DA6FF]">{article.category}</span>
<span className="text-[9px] text-[#86868B]">{new Date(article.publishedAt).toLocaleDateString(locale === 'en' ? 'en-US' : locale, { month: 'short', day: 'numeric' })}</span>
<time dateTime={new Date(article.publishedAt).toISOString()} className="text-[9px] text-[#86868B]">{new Date(article.publishedAt).toLocaleDateString(locale === 'en' ? 'en-US' : locale, { month: 'short', day: 'numeric' })}</time>
</div>
<h3 className="text-base font-medium text-[#1D1D1F] dark:text-white mb-2 leading-snug group-hover:text-[#0066CC] dark:group-hover:text-[#4DA6FF] transition-colors line-clamp-2">{article.title}</h3>
<p className="text-xs text-[#86868B] dark:text-[#A1A1A6] line-clamp-2 mb-4">{article.excerpt}</p>
@@ -122,6 +125,7 @@ export default async function NewsHub({ params }: { params: Promise<{ locale: st
</span>
</div>
</Link>
</article>
))}
</div>
)}
+6 -2
View File
@@ -37,7 +37,9 @@ export async function proxy(request: NextRequest) {
if (isPublicHQRoute) {
return NextResponse.redirect(new URL('/hq-command/dashboard', request.url));
}
return NextResponse.next(); // Pasa al CMS tranquilamente
const authedRes = NextResponse.next();
authedRes.headers.set('X-Robots-Tag', 'noindex, nofollow');
return authedRes;
} catch (error) {
// Pase falso o expirado
if (!isPublicHQRoute) {
@@ -45,7 +47,9 @@ export async function proxy(request: NextRequest) {
}
}
}
return NextResponse.next(); // Pasa a login/setup si no hay cookie
const hqRes = NextResponse.next();
hqRes.headers.set('X-Robots-Tag', 'noindex, nofollow');
return hqRes;
}
// --------------------------------------------------------