mirror of
https://github.com/Such-Software/such.software.git
synced 2026-10-17 05:32:05 +02:00
home: drop Selected Works card + juice hero; per-pillar OG cards; sitemap freshness
Home: the big empty 'Selected Works' section-container card is gone in favour of a compact centered heading, pulling the bento up ~300px so the work leads directly under the hero. Hero juice (all LCP-safe + motion-safe): the H1 gets an animated gradient sheen (only the gradient POSITION animates, so the largest paint is never delayed; verified opacity stays 1), a soft breathing cyan aura sits behind it (aria-hidden), and the CTAs get a cyan glow + hover lift. Services: each pillar page (and the hub) now generates its own OG social card via the shared gameOgImage helper with a 'Services' badge and the pillar accent, so sharing a pillar shows a distinct card instead of the generic banner. Verified: distinct per-slug image URLs, valid 1200x630 PNGs. Sitemap: bump lastModified 2026-06-01 -> 2026-07-12 (we've meaningfully changed the site).
This commit is contained in:
@@ -282,3 +282,22 @@
|
|||||||
}
|
}
|
||||||
@keyframes nebula-caret { 0%, 50% { border-color: currentColor; } 50.01%, 100% { border-color: transparent; } }
|
@keyframes nebula-caret { 0%, 50% { border-color: currentColor; } 50.01%, 100% { border-color: transparent; } }
|
||||||
@keyframes nebula-term-out { to { opacity: 0; } }
|
@keyframes nebula-term-out { to { opacity: 0; } }
|
||||||
|
|
||||||
|
/* Hero title: an animated gradient sheen. LCP-safe — the text paints immediately with the
|
||||||
|
gradient; only the gradient POSITION animates, which never delays the largest paint. The
|
||||||
|
global reduced-motion rule freezes it to the base gradient. */
|
||||||
|
.hero-title {
|
||||||
|
background-image: linear-gradient(110deg, #22d3ee 0%, #3b82f6 30%, #7dd3fc 50%, #3b82f6 70%, #22d3ee 100%);
|
||||||
|
background-size: 220% auto;
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
animation: hero-sheen 9s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes hero-sheen {
|
||||||
|
0%, 100% { background-position: 0% center; }
|
||||||
|
50% { background-position: 100% center; }
|
||||||
|
}
|
||||||
|
/* Soft aura behind the hero title (decorative). Gentle breathing scale/opacity. */
|
||||||
|
@keyframes hero-aura { 0%, 100% { opacity: 0.55; transform: translateX(-50%) scale(1); } 50% { opacity: 0.8; transform: translateX(-50%) scale(1.08); } }
|
||||||
|
.hero-aura { animation: hero-aura 7s ease-in-out infinite; }
|
||||||
+13
-13
@@ -16,11 +16,16 @@ export default function Home() {
|
|||||||
<HomeShell>
|
<HomeShell>
|
||||||
<div
|
<div
|
||||||
id="main-content"
|
id="main-content"
|
||||||
className="z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-44 sm:pt-32 pb-10 px-4 text-center scroll-mt-28"
|
className="relative z-10 w-full max-w-7xl mx-auto flex flex-col items-center pt-40 sm:pt-28 pb-10 px-4 text-center scroll-mt-28"
|
||||||
>
|
>
|
||||||
|
{/* Decorative aura behind the title (aria-hidden, LCP-safe, motion-safe). */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
className="hero-aura pointer-events-none absolute left-1/2 top-24 sm:top-16 -z-10 h-64 w-[46rem] max-w-[92vw] rounded-full bg-cyan-400/25 blur-3xl"
|
||||||
|
/>
|
||||||
<h1
|
<h1
|
||||||
id="hero-title"
|
id="hero-title"
|
||||||
className="text-4xl sm:text-5xl md:text-7xl font-bold mb-6 bg-clip-text text-transparent bg-gradient-to-r from-cyan-400 to-blue-600 pb-2"
|
className="hero-title text-4xl sm:text-5xl md:text-7xl font-bold mb-6 pb-2"
|
||||||
style={{ maxWidth: "min(calc(90vw - 120px), 100%)" }}
|
style={{ maxWidth: "min(calc(90vw - 120px), 100%)" }}
|
||||||
>
|
>
|
||||||
<span className="block">Precision Engineering</span>
|
<span className="block">Precision Engineering</span>
|
||||||
@@ -30,24 +35,19 @@ export default function Home() {
|
|||||||
Custom e-commerce, websites, apps, and consulting. Plus our own mobile apps and games. A software studio in Kennett Square, PA.
|
Custom e-commerce, websites, apps, and consulting. Plus our own mobile apps and games. A software studio in Kennett Square, PA.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="flex gap-4 mb-10">
|
<div className="flex flex-wrap justify-center gap-4 mb-14">
|
||||||
<Link href="/products">
|
<Link href="/products">
|
||||||
<Button size="lg" className="rounded-full">View Portfolio</Button>
|
<Button size="lg" className="rounded-full shadow-lg shadow-cyan-500/25 hover:shadow-cyan-400/40 hover:scale-[1.03] transition-all">View Portfolio</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="#contact">
|
<Link href="#contact">
|
||||||
<Button variant="outline" size="lg" className="rounded-full">Start a Project</Button>
|
<Button variant="outline" size="lg" className="rounded-full hover:scale-[1.03] transition-all">Start a Project</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Show before tell: the visual work leads, the services copy supports it below.
|
{/* Show, don't tell: the bento leads directly under the hero (the Selected Works
|
||||||
(The old order opened with two screens of text before the first image.) */}
|
header card was dropped to pull the work up higher). */}
|
||||||
<ScrollReveal className="w-full max-w-6xl mx-auto mb-16 px-4">
|
<ScrollReveal className="w-full max-w-6xl mx-auto mb-16 px-4">
|
||||||
<div className="text-left mb-8 section-container border-l-4 border-l-blue-500">
|
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight text-foreground mb-6">Selected Works</h2>
|
||||||
<h2 className="text-3xl font-bold mb-3 tracking-tight text-foreground">Selected Works</h2>
|
|
||||||
<p className="text-muted-foreground text-lg max-w-3xl leading-relaxed">
|
|
||||||
Recent projects spanning e-commerce, games, payment infrastructure, and interactive web applications.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<PortfolioGrid />
|
<PortfolioGrid />
|
||||||
</ScrollReveal>
|
</ScrollReveal>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { gameOgImage, OG_SIZE, OG_CONTENT_TYPE } from "@/lib/og";
|
||||||
|
import { pillars, pillarBySlug } from "@/lib/services";
|
||||||
|
|
||||||
|
export const alt = "Such Software services";
|
||||||
|
export const size = OG_SIZE;
|
||||||
|
export const contentType = OG_CONTENT_TYPE;
|
||||||
|
|
||||||
|
// Prebuild one card per pillar (matches the page's dynamicParams=false).
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return pillars.map((p) => ({ pillar: p.slug }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hex accents for the OG card (the pages use Tailwind stems; ImageResponse needs literal colours).
|
||||||
|
const ogAccent: Record<string, string> = {
|
||||||
|
commerce: "#3b82f6", // blue
|
||||||
|
games: "#a855f7", // purple
|
||||||
|
crypto: "#22d3ee", // cyan
|
||||||
|
education: "#f59e0b", // amber
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Image({ params }: { params: Promise<{ pillar: string }> }) {
|
||||||
|
const { pillar: slug } = await params;
|
||||||
|
const pillar = pillarBySlug(slug);
|
||||||
|
if (!pillar) return gameOgImage({ title: "Services", tagline: "Such Software", accent: "#34d399", badge: "Services" });
|
||||||
|
return gameOgImage({
|
||||||
|
title: pillar.title,
|
||||||
|
tagline: pillar.card,
|
||||||
|
accent: ogAccent[pillar.slug] ?? "#34d399",
|
||||||
|
badge: "Services",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { gameOgImage, OG_SIZE, OG_CONTENT_TYPE } from "@/lib/og";
|
||||||
|
|
||||||
|
export const alt = "Such Software — four things, done well";
|
||||||
|
export const size = OG_SIZE;
|
||||||
|
export const contentType = OG_CONTENT_TYPE;
|
||||||
|
|
||||||
|
export default function Image() {
|
||||||
|
return gameOgImage({
|
||||||
|
title: "Four things, done well",
|
||||||
|
tagline: "Commerce & web, games & apps, crypto & payments, and PhD-led science education.",
|
||||||
|
accent: "#34d399",
|
||||||
|
badge: "Services",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
|
|||||||
// every page as "modified now" on every build, which is noise that dilutes the freshness
|
// every page as "modified now" on every build, which is noise that dilutes the freshness
|
||||||
// signal. Bump this when the site's static content meaningfully changes. (Blog posts carry
|
// signal. Bump this when the site's static content meaningfully changes. (Blog posts carry
|
||||||
// their own per-post dates below.)
|
// their own per-post dates below.)
|
||||||
const updated = new Date("2026-06-01");
|
const updated = new Date("2026-07-12");
|
||||||
|
|
||||||
const staticPages: MetadataRoute.Sitemap = [
|
const staticPages: MetadataRoute.Sitemap = [
|
||||||
{ url: base, lastModified: updated, changeFrequency: "weekly", priority: 1.0 },
|
{ url: base, lastModified: updated, changeFrequency: "weekly", priority: 1.0 },
|
||||||
|
|||||||
Reference in New Issue
Block a user