import { ImageResponse } from '@vercel/og';
import { NextRequest } from 'next/server';
import {
AnisotaIcon,
DropChevron,
Headline,
loadGoogleFont,
OgFrame,
OG_COLORS,
TopRow,
UrlPill,
WaypointRow,
} from '@/lib/og-design';
import type { ReactNode } from 'react';
export const runtime = 'edge';
export const revalidate = 86400;
type PageConfig = {
eyebrow: string;
title: string;
tagline: string;
/** Promotional product visual shown beneath the headline. */
visual: ReactNode;
};
/**
* Promotional OG cards for the site's landing pages. Every card shares the
* same top row (leaf wordmark + uppercase product label), a serif headline,
* a tagline, and a product-specific visual mock further down.
*/
function configFor(page: string): PageConfig {
switch (page) {
case 'explore':
return {
eyebrow: 'Atmosphere data explorer',
title: 'Browse every PDS.',
tagline:
'Records, identity history, backlinks, and a live view of network activity. For any account in the Atmosphere.',
visual: ,
};
case 'extension':
return {
eyebrow: 'Browser extension',
title: 'Jump between clients\nin one click.',
tagline:
'Land on any post and pop open the curated picker. Auto-redirect by lexicon, inspect the underlying AT URI, copy a universal link.',
visual: ,
};
case 'universal-links':
return {
eyebrow: 'Universal links',
title: 'One link, every\nAtmosphere client.',
tagline:
'Share aturi.to/handle/collection/rkey with anyone — they pick where to open it, from a curated list of 25+ clients.',
visual: ,
};
case 'integrate':
return {
eyebrow: 'Integrate',
title: 'Add universal sharing\nto your app.',
tagline:
'A simple URL pattern your atproto app can adopt to make every record shareable across the Atmosphere.',
visual: ,
};
case 'fork':
return {
eyebrow: 'Fork & deploy',
title: 'Run your own\ninstance.',
tagline:
'Open source, environment-driven branding, and ready to deploy on a custom domain.',
visual: ,
};
case 'home':
default:
return {
eyebrow: 'Atmosphere fast travel',
title: 'Tour the\nAtmosphere.',
tagline:
'Travel between clients with the browser extension, share universal links, and explore any account’s PDS data.',
visual: ,
};
}
}
// ─── Page visuals ──────────────────────────────────────────────────────────
function HomeVisual() {
// Mirrors the homepage WaypointJumpVisual: aturi.to URL pill on top,
// chevron, row of waypoint icons with one highlighted to show the
// "which client?" pick that aturi enables.
return (
);
}
function UniversalLinksVisual() {
return ;
}
function ExploreVisual() {
// AT-URI styled breadcrumb — the explorer's signature navigation
// pattern, rendered as a horizontal pill so it scans like the
// breadcrumb you'd see inside the app.
const segments: { label: string; muted?: boolean }[] = [
{ label: 'pds.atpota.to' },
{ label: '@dame.is' },
{ label: 'app.bsky.actor.profile' },
{ label: 'self', muted: true },
];
return (
{segments.map((s, i) => (
{i > 0 && (
›
)}
{s.label}
))}
);
}
function ExtensionVisual() {
// Compact extension-popup mock: header strip, source URI, a
// "Recommended" row with the Anisota icon (matches the live popup's
// default recommendation for posts).
return (
{/* Popup header */}
{/* Source URI strip */}
at://
dame.is / app.bsky.feed.post / 3lq9…
{/* Recommended section */}
★
Recommended for posts
} featured />
);
}
function RowChip({
name,
desc,
icon,
featured,
}: {
name: string;
desc: string;
icon: ReactNode;
featured?: boolean;
}) {
return (
);
}
// ─── Route ────────────────────────────────────────────────────────────────
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const page = searchParams.get('page') || 'home';
const config = configFor(page);
const allText =
`${config.title} ${config.tagline} ${config.eyebrow} aturi.to ` +
'Anisota Bluesky Leaflet Tangled Margin Deer Grain Recommended for posts ' +
'pds.atpota.to dame.is app.bsky.feed.post app.bsky.actor.profile self at:// › ★ ▾';
const fontData = await loadGoogleFont('Crimson+Pro:wght@300;400;600', allText);
return new ImageResponse(
(
),
{
width: 1200,
height: 630,
fonts: [
{
name: 'Crimson Pro',
data: fontData,
style: 'normal',
weight: 300,
},
],
},
);
} catch (error) {
console.error('Error generating OG image:', error);
return new Response('Error generating image', { status: 500 });
}
}