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:
@@ -281,4 +281,23 @@
|
||||
100% { width: 0; }
|
||||
}
|
||||
@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>
|
||||
<div
|
||||
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
|
||||
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%)" }}
|
||||
>
|
||||
<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.
|
||||
</p>
|
||||
|
||||
<div className="flex gap-4 mb-10">
|
||||
<div className="flex flex-wrap justify-center gap-4 mb-14">
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
{/* Show before tell: the visual work leads, the services copy supports it below.
|
||||
(The old order opened with two screens of text before the first image.) */}
|
||||
{/* Show, don't tell: the bento leads directly under the hero (the Selected Works
|
||||
header card was dropped to pull the work up higher). */}
|
||||
<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-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>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold tracking-tight text-foreground mb-6">Selected Works</h2>
|
||||
<PortfolioGrid />
|
||||
</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
|
||||
// signal. Bump this when the site's static content meaningfully changes. (Blog posts carry
|
||||
// their own per-post dates below.)
|
||||
const updated = new Date("2026-06-01");
|
||||
const updated = new Date("2026-07-12");
|
||||
|
||||
const staticPages: MetadataRoute.Sitemap = [
|
||||
{ url: base, lastModified: updated, changeFrequency: "weekly", priority: 1.0 },
|
||||
|
||||
Reference in New Issue
Block a user