import { CtaBanner } from '@/app/(home)/_sections/cta-banner'; import { HeroContainer } from '@/app/(home)/_sections/hero'; import { Testimonials } from '@/app/(home)/_sections/testimonials'; import { ArticleCard } from '@/components/article-card'; import { FeatureCardContainer } from '@/components/feature-card'; import { GetStartedButton } from '@/components/get-started-button'; import { Logo } from '@/components/logo'; import { SectionHeader } from '@/components/section'; import { Toc } from '@/components/toc'; import { url, getAuthor } from '@/lib/layout.shared'; import { getOgImageUrl, getPageMetadata } from '@/lib/metadata'; import { articleSource } from '@/lib/source'; import { getMDXComponents } from '@/mdx-components'; import { ArrowLeftIcon } from 'lucide-react'; import type { Metadata } from 'next'; import Image from 'next/image'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import Script from 'next/script'; export async function generateStaticParams() { const articles = await articleSource.getPages(); return articles.map((article) => { // Extract slug from URL (e.g., '/articles/my-article' -> 'my-article') const slug = article.url.replace(/^\/articles\//, '').replace(/\/$/, ''); return { articleSlug: slug }; }); } export async function generateMetadata({ params, }: { params: Promise<{ articleSlug: string }>; }): Promise { const { articleSlug } = await params; const article = await articleSource.getPage([articleSlug]); const author = getAuthor(article?.data.team); if (!article) { return { title: 'Article Not Found', }; } return getPageMetadata({ title: article.data.title, description: article.data.description, url: url(article.url), image: getOgImageUrl(article.url), }); } export default async function Page({ params, }: { params: Promise<{ articleSlug: string }>; }) { const { articleSlug } = await params; const article = await articleSource.getPage([articleSlug]); const Body = article?.data.body; const author = getAuthor(article?.data.team); const goBackUrl = '/articles'; const relatedArticles = (await articleSource.getPages()) .filter( (item) => item.data.tag === article?.data.tag && item.url !== article?.url, ) .sort((a, b) => b.data.date.getTime() - a.data.date.getTime()); if (!Body) { return notFound(); } // Create the JSON-LD data const jsonLd = { '@context': 'https://schema.org', '@type': 'Article', headline: article?.data.title, datePublished: article?.data.date.toISOString(), dateModified: article?.data.updated?.toISOString() || article?.data.date.toISOString(), author: { '@type': 'Person', name: author.name, }, publisher: { '@type': 'Organization', name: 'OpenPanel', logo: { '@type': 'ImageObject', url: url('/logo.png'), }, }, mainEntityOfPage: { '@type': 'WebPage', '@id': url(article.url), }, image: { '@type': 'ImageObject', url: url(article.data.cover), }, }; return (
Back to all articles
{author.image ? ( {author.name} ) : ( )}

{author.name}

{article?.data.date.toLocaleDateString()}

{article?.data.updated && (

Updated on {article?.data.updated.toLocaleDateString()}

)}