Files
stats/apps/public/src/app/og/[...og]/route.tsx
Carl-Gerhard Lindesvärd 6ce9b5dd1b public: feature pages
2026-02-07 16:42:02 +00:00

300 lines
8.8 KiB
TypeScript

import { getAllCompareSlugs, getCompareData } from '@/lib/compare';
import { getFeatureData } from '@/lib/features';
import { url as baseUrl } from '@/lib/layout.shared';
import { articleSource, guideSource, pageSource, source } from '@/lib/source';
import { ImageResponse } from 'next/og';
import type { NextRequest } from 'next/server';
// Truncate text helper
function truncateText(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return `${text.substring(0, maxLength).trim()}...`;
}
async function getOgData(
segments: string[],
): Promise<{ title: string; description?: string }> {
switch (segments[0]) {
case 'default':
return {
title: 'Home',
description: 'Home page',
};
case 'supporter': {
return {
title: 'Become a Supporter',
description:
'Support OpenPanel and get exclusive perks like latest Docker images, prioritized support, and early access to new features.',
};
}
case 'open-source': {
return {
title: 'Free analytics for open source projects',
description:
"Get free web and product analytics for your open source project. Track up to 2.5M events/month. Apply to OpenPanel's open source program today.",
};
}
case 'pricing': {
return {
title: 'Pricing',
description:
'Our pricing is as simple as it gets, choose how many events you want to track each month, everything else is unlimited, no tiers, no hidden costs.',
};
}
case 'articles': {
if (segments.length > 1) {
const data = await articleSource.getPage(segments.slice(1));
return {
title: data?.data.title ?? 'Article Not Found',
description:
data?.data.description || 'Whooops, could not find this article',
};
}
return {
title: 'Articles',
description:
'Read our latest articles and stay up to date with the latest news and updates.',
};
}
case 'compare': {
const slug = segments[1];
if (!slug) {
return {
title: 'Compare alternatives',
description: 'Compare OpenPanel with other analytics tools',
};
}
const data = await getCompareData(slug);
return {
title: data?.seo.title || data?.hero.heading || 'Compare',
description: data?.seo.description || data?.hero.subheading,
};
}
case 'guides': {
if (segments.length > 1) {
const data = await guideSource.getPage(segments.slice(1));
return {
title: data?.data.title ?? 'Guide Not Found',
description:
data?.data.description || 'Whooops, could not find this guide',
};
}
return {
title: 'Implementation Guides',
description: 'Step-by-step tutorials for adding analytics to your app',
};
}
case 'features': {
const slug = segments[1];
if (!slug) {
return {
title: 'Product analytics features',
description:
'Explore OpenPanel features: event tracking, funnels, retention, user profiles, and more.',
};
}
const featureData = await getFeatureData(slug);
return {
title: featureData?.seo.title ?? 'Feature Not Found',
description:
featureData?.seo.description ?? featureData?.hero.subheading,
};
}
case 'docs': {
const data = await source.getPage(segments.slice(1));
return {
title: data?.data.title ?? 'Page Not Found',
description:
data?.data.description || 'Whooops, could not find this page',
};
}
case 'tools': {
if (segments.length > 1) {
const tool = segments[1];
switch (tool) {
case 'ip-lookup':
return {
title: 'IP Lookup Tool',
description:
'Find detailed information about any IP address including geolocation, ISP, and network details.',
};
case 'url-checker':
return {
title: 'URL Checker',
description:
'Analyze any website for SEO, social media, technical, and security information. Get comprehensive insights about any URL.',
};
default:
return {
title: 'Tools',
description: 'Free web tools for developers and website owners',
};
}
}
return {
title: 'Tools',
description: 'Free web tools for developers and website owners',
};
}
default: {
const data = await pageSource.getPage(segments);
return {
title: data?.data.title || 'Page Not Found',
description:
data?.data.description || 'Whooops, could not find this page',
};
}
}
}
// export async function generateStaticParams() {
// const params: { og: string[] }[] = [];
// // Static pages
// params.push({ og: ['default'] });
// params.push({ og: ['supporter'] });
// params.push({ og: ['pricing'] });
// params.push({ og: ['articles'] });
// params.push({ og: ['compare'] });
// params.push({ og: ['docs'] });
// // Articles
// const articles = await articleSource.getPages();
// for (const article of articles) {
// const slug = article.url.replace(/^\/articles\//, '').replace(/\/$/, '');
// params.push({ og: ['articles', slug] });
// }
// // Compare pages
// const compareSlugs = await getAllCompareSlugs();
// for (const slug of compareSlugs) {
// params.push({ og: ['compare', slug] });
// }
// // Docs pages
// const docs = await source.getPages();
// for (const doc of docs) {
// params.push({ og: ['docs', ...doc.slugs] });
// }
// // Other pages
// const pages = await pageSource.getPages();
// for (const page of pages) {
// params.push({ og: page.slugs });
// }
// return params;
// }
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ og: string[] }> },
) {
try {
const { og } = await params;
// Get OG data based on segments
const { title, description } = await getOgData(og);
// Truncate title and description
const truncatedTitle = truncateText(title, 100);
const truncatedDescription = description
? truncateText(description, 200)
: undefined;
// Get background image URL
const backgroundImageUrl = baseUrl('/ogimage-empty.png');
// Fetch Geist font files from CDN (cache fonts for better performance)
const [geistRegular, geistBold] = await Promise.all([
fetch(
'https://cdn.jsdelivr.net/npm/geist@1.5.1/dist/fonts/geist-sans/Geist-Regular.ttf',
).then((res) => res.arrayBuffer()),
fetch(
'https://cdn.jsdelivr.net/npm/geist@1.5.1/dist/fonts/geist-sans/Geist-Bold.ttf',
).then((res) => res.arrayBuffer()),
]);
return new ImageResponse(
<div
style={{
height: '100%',
width: '100%',
position: 'relative',
backgroundImage: `url(${backgroundImageUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
display: 'flex',
}}
>
{/* Title and Description at bottom left */}
<div
style={{
bottom: '55px',
left: '55px',
maxWidth: '900px',
display: 'flex',
flexDirection: 'column',
marginTop: 'auto',
}}
>
{/* Title */}
<div
style={{
fontSize: truncatedTitle.length > 40 ? '56px' : '72px',
fontFamily: 'GeistBold',
color: '#000',
lineHeight: 1.1,
marginBottom: truncatedDescription ? '20px' : '0',
fontWeight: 700,
}}
>
{truncatedTitle}
</div>
{/* Description */}
{truncatedDescription ? (
<div
style={{
fontSize: '30px',
fontFamily: 'Geist',
color: '#666',
lineHeight: 1.4,
fontWeight: 400,
}}
>
{truncatedDescription}
</div>
) : null}
</div>
</div>,
{
headers: {
'Cache-Control': 'public, max-age=3600, stale-while-revalidate=86400',
},
width: 1200,
height: 630,
fonts: [
{
name: 'Geist',
data: geistRegular,
style: 'normal',
weight: 400,
},
{
name: 'GeistBold',
data: geistBold,
style: 'normal',
weight: 700,
},
],
},
);
} catch (e: any) {
console.error(`Failed to generate OG image: ${e.message}`);
return new Response('Failed to generate the image', {
status: 500,
});
}
}