Magazine Cover
Full-bleed photo top with vol. label, circular avatar overlapping the bottom edge, Fraunces serif name. Below: a 4-platform follower count strip + book promo tile + group-program cohort tile with progress bar + dark freebie email-grab card + plain link buttons + social row.
Preview
Source
tsx
"use client";
import { useState } from "react";
import {
Instagram,
Twitter,
Youtube,
Mail,
Globe,
ShoppingBag,
PlayCircle,
Linkedin,
Music2,
ArrowRight,
ArrowUpRight,
Calendar,
Sparkles,
Check,
CheckCircle2,
type LucideIcon,
} from "lucide-react";
import type { LinkIcon, LinksInBioData, RichBlock } from "../types";
import { defaultData } from "../defaultData";
const ICONS: Record<LinkIcon, LucideIcon> = {
instagram: Instagram,
twitter: Twitter,
tiktok: Music2,
youtube: Youtube,
spotify: Music2,
apple: Music2,
linkedin: Linkedin,
email: Mail,
globe: Globe,
shop: ShoppingBag,
play: PlayCircle,
};
const INK = "#1c1c20";
const PAPER = "#fbfaf6";
const ACCENT = "#d6502a";
const SOFT = "#efeae0";
function BookTile({ block }: { block: Extract<RichBlock, { kind: "book" }> }) {
return (
<a
href={block.href}
className="group flex gap-4 overflow-hidden rounded-2xl border bg-white p-4 transition-all hover:-translate-y-0.5 hover:shadow-[0_18px_44px_rgba(28,28,32,0.10)]"
style={{ borderColor: "rgba(28,28,32,0.08)" }}
>
<div
className="relative h-32 w-22 shrink-0 overflow-hidden rounded-md shadow-md"
style={{ width: "88px", height: "128px", boxShadow: "0 14px 28px rgba(28,28,32,0.18)" }}
>
<img
src={block.cover}
alt={block.title}
width={300}
height={450}
loading="lazy"
className="h-full w-full object-cover"
/>
</div>
<div className="flex min-w-0 flex-1 flex-col">
<p className="text-[10px] font-semibold uppercase tracking-[0.22em]" style={{ color: ACCENT }}>
New book
</p>
<p
className="mt-1 line-clamp-2 text-base leading-snug"
style={{ fontFamily: "'Fraunces', serif", color: INK, fontWeight: 600 }}
>
{block.title}
</p>
{block.blurb && (
<p className="mt-1 line-clamp-2 text-xs" style={{ color: "#5b5b65" }}>
{block.blurb}
</p>
)}
<div className="mt-auto flex items-center justify-between pt-2">
{block.price && (
<span className="text-sm font-bold" style={{ color: INK }}>
{block.price}
</span>
)}
<span
className="inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold"
style={{ background: INK, color: PAPER }}
>
Get the book <ArrowRight className="h-3 w-3" />
</span>
</div>
</div>
</a>
);
}
function GroupProgramTile({
block,
}: {
block: Extract<RichBlock, { kind: "group-program" }>;
}) {
const filled = block.slotsFilled ?? 0;
const total = block.slotsTotal ?? 0;
const pct = total > 0 ? Math.min(100, Math.round((filled / total) * 100)) : 0;
const remaining = Math.max(0, total - filled);
return (
<a
href={block.href}
className="group block overflow-hidden rounded-2xl border bg-white p-5 transition-all hover:-translate-y-0.5 hover:shadow-[0_18px_44px_rgba(28,28,32,0.10)]"
style={{ borderColor: "rgba(28,28,32,0.08)" }}
>
<div className="flex items-start justify-between gap-3">
<div>
<span
className="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.2em]"
style={{ background: SOFT, color: INK }}
>
<Calendar className="h-3 w-3" /> Group program
</span>
<p
className="mt-2 text-base leading-snug"
style={{ fontFamily: "'Fraunces', serif", fontWeight: 600, color: INK }}
>
{block.title}
</p>
<p className="mt-1 text-xs" style={{ color: "#5b5b65" }}>
Next cohort begins {block.startDate}
</p>
</div>
<ArrowUpRight className="h-5 w-5 shrink-0" style={{ color: INK }} />
</div>
{block.features && block.features.length > 0 && (
<ul className="mt-3 grid gap-1.5">
{block.features.slice(0, 3).map((f) => (
<li key={f} className="flex items-start gap-2 text-xs" style={{ color: INK }}>
<Check className="mt-0.5 h-3.5 w-3.5 shrink-0" style={{ color: ACCENT }} />
<span>{f}</span>
</li>
))}
</ul>
)}
{total > 0 && (
<div className="mt-4">
<div className="flex items-center justify-between text-[11px]" style={{ color: "#5b5b65" }}>
<span>
{filled} / {total} seats filled
</span>
<span style={{ color: ACCENT, fontWeight: 600 }}>
{remaining} left
</span>
</div>
<div
className="mt-1.5 h-1.5 overflow-hidden rounded-full"
style={{ background: SOFT }}
>
<div
className="h-full rounded-full transition-all"
style={{ background: ACCENT, width: `${pct}%` }}
/>
</div>
</div>
)}
<span
className="mt-4 inline-flex items-center gap-1 rounded-full px-4 py-2 text-sm font-semibold"
style={{ background: ACCENT, color: PAPER }}
>
{block.ctaLabel ?? "Apply"} <ArrowRight className="h-3.5 w-3.5" />
</span>
</a>
);
}
function FreebieTile({ block }: { block: Extract<RichBlock, { kind: "freebie" }> }) {
const [email, setEmail] = useState("");
const [submitted, setSubmitted] = useState(false);
return (
<div
className="overflow-hidden rounded-2xl p-5"
style={{ background: INK, color: PAPER }}
>
<span
className="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.2em]"
style={{ background: "rgba(255,255,255,0.12)", color: PAPER }}
>
<Sparkles className="h-3 w-3" /> Free
</span>
<p
className="mt-2 text-lg leading-snug"
style={{ fontFamily: "'Fraunces', serif", fontWeight: 600 }}
>
{block.title}
</p>
{block.description && (
<p className="mt-1.5 text-sm" style={{ color: "rgba(255,255,255,0.7)" }}>
{block.description}
</p>
)}
{submitted ? (
<div className="mt-3 flex items-center gap-2 rounded-lg bg-white/10 px-3 py-2.5 text-sm">
<CheckCircle2 className="h-4 w-4" style={{ color: "#7ce695" }} />
<span>Check your inbox — heading your way now.</span>
</div>
) : (
<form
onSubmit={(e) => {
e.preventDefault();
if (email) setSubmitted(true);
}}
className="mt-3 flex gap-2"
>
<input
type="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder={block.placeholder ?? "[email protected]"}
className="min-w-0 flex-1 rounded-lg border border-white/15 bg-white/5 px-3 py-2.5 text-sm text-white placeholder:text-white/40 focus:border-white/40 focus:outline-none"
/>
<button
type="submit"
className="shrink-0 rounded-lg px-4 py-2.5 text-sm font-semibold"
style={{ background: ACCENT, color: PAPER }}
>
{block.cta ?? "Get it"}
</button>
</form>
)}
{block.socialProof && !submitted && (
<p className="mt-2 text-[11px]" style={{ color: "rgba(255,255,255,0.5)" }}>
{block.socialProof}
</p>
)}
</div>
);
}
export interface LinksInBioMagazineCoverProps {
data?: LinksInBioData;
}
export default function LinksInBioMagazineCover({
data = defaultData,
}: LinksInBioMagazineCoverProps) {
const heroImage = data.heroImage ?? data.avatar;
const book = data.richBlocks?.find(
(b): b is Extract<RichBlock, { kind: "book" }> => b.kind === "book"
);
const program = data.richBlocks?.find(
(b): b is Extract<RichBlock, { kind: "group-program" }> => b.kind === "group-program"
);
const freebie = data.richBlocks?.find(
(b): b is Extract<RichBlock, { kind: "freebie" }> => b.kind === "freebie"
);
return (
<>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..900;1,9..144,300..900&family=Inter:wght@400;500;600;700&display=swap"
/>
<section
className="min-h-screen w-full pb-12"
style={{ background: PAPER, color: INK, fontFamily: "'Inter', sans-serif" }}
>
{/* Hero photo */}
<div className="relative h-72 w-full overflow-hidden sm:h-80">
<img
src={heroImage}
alt=""
width={1600}
height={800}
loading="eager"
className="h-full w-full object-cover"
/>
<div
aria-hidden
className="absolute inset-0"
style={{
background:
"linear-gradient(180deg, rgba(28,28,32,0) 50%, rgba(28,28,32,0.55) 100%)",
}}
/>
{/* Issue label */}
<div className="absolute left-5 top-5 flex items-center gap-2">
<span
className="rounded-full px-3 py-1 text-[10px] font-semibold uppercase tracking-[0.25em]"
style={{ background: PAPER, color: INK }}
>
Vol. 12 · {new Date().getFullYear()}
</span>
</div>
</div>
{/* Avatar overlap + identity */}
<div className="relative mx-auto -mt-14 w-full max-w-[460px] px-5 sm:-mt-16">
<div className="flex flex-col items-center text-center">
<img
src={data.avatar}
alt={data.name}
width={128}
height={128}
loading="eager"
className="h-28 w-28 rounded-full object-cover sm:h-32 sm:w-32"
style={{
border: `4px solid ${PAPER}`,
boxShadow: "0 16px 36px rgba(28,28,32,0.18)",
}}
/>
<h1
className="mt-4 leading-[1.05]"
style={{
fontFamily: "'Fraunces', serif",
fontWeight: 700,
fontSize: "clamp(2rem, 6vw, 2.5rem)",
letterSpacing: "-0.01em",
}}
>
{data.name}
{data.verified && <span className="ml-2 align-middle text-base" style={{ color: ACCENT }}>✓</span>}
</h1>
{data.handle && (
<p className="mt-1 text-sm font-medium" style={{ color: "#5b5b65" }}>
{data.handle}
</p>
)}
{data.bio && (
<p
className="mt-3 max-w-[360px] text-[15px] leading-relaxed"
style={{ color: "#3a3a40" }}
>
{data.bio}
</p>
)}
</div>
{/* Social proof row */}
{data.socialProof && data.socialProof.length > 0 && (
<div
className="mt-6 grid grid-cols-4 gap-2 rounded-2xl border p-3"
style={{ borderColor: "rgba(28,28,32,0.08)", background: "white" }}
>
{data.socialProof.slice(0, 4).map((s) => {
const Icon = ICONS[s.platform] ?? Globe;
return (
<div key={s.platform} className="flex flex-col items-center gap-1">
<Icon className="h-4 w-4" style={{ color: INK }} strokeWidth={1.6} />
<span className="text-sm font-bold tracking-tight" style={{ color: INK }}>
{s.count}
</span>
</div>
);
})}
</div>
)}
{/* Tile mix */}
<div className="mt-6 space-y-3">
{book && <BookTile block={book} />}
{program && <GroupProgramTile block={program} />}
{freebie && <FreebieTile block={freebie} />}
{/* Plain link buttons */}
{data.links.slice(0, 4).map((link) => {
const Icon = link.icon ? ICONS[link.icon] : ArrowUpRight;
return (
<a
key={link.label}
href={link.href}
className="group flex items-center gap-3 rounded-2xl border bg-white px-4 py-3.5 transition-all hover:-translate-y-0.5 hover:shadow-[0_10px_28px_rgba(28,28,32,0.08)]"
style={{ borderColor: "rgba(28,28,32,0.08)" }}
>
<span
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full"
style={{ background: SOFT }}
>
<Icon className="h-4 w-4" style={{ color: INK }} strokeWidth={1.6} />
</span>
<span className="flex-1 truncate text-sm font-semibold" style={{ color: INK }}>
{link.label}
</span>
{link.badge && (
<span
className="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider"
style={{ background: ACCENT, color: PAPER }}
>
{link.badge}
</span>
)}
<ArrowUpRight
className="h-4 w-4 shrink-0 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
style={{ color: INK, opacity: 0.4 }}
/>
</a>
);
})}
</div>
{/* Socials row */}
{data.socials && data.socials.length > 0 && (
<div className="mt-8 flex items-center justify-center gap-4" style={{ color: "#5b5b65" }}>
{data.socials.map((s, i) => {
const Icon = ICONS[s.type] ?? Globe;
return (
<a
key={i}
href={s.href}
aria-label={s.type}
className="rounded-full border p-2 transition-colors hover:text-[#1c1c20]"
style={{ borderColor: "rgba(28,28,32,0.10)" }}
>
<Icon className="h-4 w-4" strokeWidth={1.6} />
</a>
);
})}
</div>
)}
<p
className="mt-8 text-center text-[10px] uppercase tracking-[0.3em]"
style={{ color: "#5b5b65" }}
>
Published & curated · {new Date().getFullYear()}
</p>
</div>
</section>
</>
);
} Claude Code Instructions
CLI Install
npx innovations add magazine-coverWhere to use it
An editorial-cover link-in-bio page for creators with multiple offers. Loads Fraunces and Inter from Google Fonts. Theme-neutral palette (paper / ink / orange accent).
Pass a typed 'data' prop (LinksInBioData from src/registry/links-in-bio/types.ts). Provide:
- heroImage — wide image used as the top hero (falls back to avatar if missing)
- socialProof — array of { platform, count } for the 4-up follower badge row
- richBlocks — opt into book / group-program / freebie tiles by adding entries with those kinds
In Astro:
import LinksInBioMagazineCover from '../components/innovations/links-in-bio/magazine-cover';
<LinksInBioMagazineCover client:load data={myProfile} />
In Next.js:
import LinksInBioMagazineCover from '@/components/innovations/links-in-bio/magazine-cover';
Best for: authors, coaches, creators with a book / cohort / lead magnet stack. The freebie tile has a built-in email form with a basic submitted state — wire it to your provider before going live.