public: add more content
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
import { FeatureCardContainer } from '@/components/feature-card';
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import type { RelatedLinks } from '@/lib/compare';
|
||||
import { ArrowRightIcon, BookOpenIcon, GitCompareIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface RelatedLinksProps {
|
||||
relatedLinks?: RelatedLinks;
|
||||
@@ -11,7 +10,7 @@ interface RelatedLinksProps {
|
||||
export function RelatedLinksSection({ relatedLinks }: RelatedLinksProps) {
|
||||
if (
|
||||
!relatedLinks ||
|
||||
(!relatedLinks.articles?.length && !relatedLinks.alternatives?.length)
|
||||
(!relatedLinks.guides?.length && !relatedLinks.articles?.length && !relatedLinks.alternatives?.length)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -19,59 +18,60 @@ export function RelatedLinksSection({ relatedLinks }: RelatedLinksProps) {
|
||||
return (
|
||||
<Section className="container">
|
||||
<SectionHeader
|
||||
description="Explore more comparisons and guides to help you choose the right analytics tool."
|
||||
title="Related resources"
|
||||
description="Explore more comparisons and guides to help you choose the right analytics tool"
|
||||
variant="sm"
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-12">
|
||||
{relatedLinks.articles && relatedLinks.articles.length > 0 && (
|
||||
<div className="mt-12 grid gap-8 md:grid-cols-3">
|
||||
{relatedLinks.guides && relatedLinks.guides.length > 0 && (
|
||||
<div className="col gap-4">
|
||||
<div className="row gap-2 items-center mb-2">
|
||||
<BookOpenIcon className="size-5 text-muted-foreground" />
|
||||
<h3 className="text-lg font-semibold">Articles</h3>
|
||||
</div>
|
||||
<div className="col gap-3">
|
||||
{relatedLinks.articles.map((article) => (
|
||||
<Link key={article.url} href={article.url}>
|
||||
<FeatureCardContainer className="hover:border-primary/30 transition-colors">
|
||||
<div className="row gap-3 items-center">
|
||||
<div className="col gap-1 flex-1 min-w-0">
|
||||
<h4 className="text-base font-semibold group-hover:text-primary transition-colors">
|
||||
{article.title}
|
||||
</h4>
|
||||
</div>
|
||||
<ArrowRightIcon className="opacity-0 group-hover:opacity-100 size-4 shrink-0 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all duration-300" />
|
||||
</div>
|
||||
</FeatureCardContainer>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<h3 className="font-semibold text-muted-foreground text-sm uppercase tracking-wider">
|
||||
Guides
|
||||
</h3>
|
||||
{relatedLinks.guides.map((guide) => (
|
||||
<Link
|
||||
className="row items-center gap-2 text-sm transition-colors hover:text-primary"
|
||||
href={guide.url}
|
||||
key={guide.url}
|
||||
>
|
||||
<ArrowRightIcon className="size-4 shrink-0" />
|
||||
{guide.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{relatedLinks.articles && relatedLinks.articles.length > 0 && (
|
||||
<div className="col gap-4">
|
||||
<h3 className="font-semibold text-muted-foreground text-sm uppercase tracking-wider">
|
||||
Articles
|
||||
</h3>
|
||||
{relatedLinks.articles.map((article) => (
|
||||
<Link
|
||||
className="row items-center gap-2 text-sm transition-colors hover:text-primary"
|
||||
href={article.url}
|
||||
key={article.url}
|
||||
>
|
||||
<ArrowRightIcon className="size-4 shrink-0" />
|
||||
{article.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{relatedLinks.alternatives && relatedLinks.alternatives.length > 0 && (
|
||||
<div className="col gap-4">
|
||||
<div className="row gap-2 items-center mb-2">
|
||||
<GitCompareIcon className="size-5 text-muted-foreground" />
|
||||
<h3 className="text-lg font-semibold">Other comparisons</h3>
|
||||
</div>
|
||||
<div className="col gap-3">
|
||||
{relatedLinks.alternatives.map((alternative) => (
|
||||
<Link key={alternative.url} href={alternative.url}>
|
||||
<FeatureCardContainer className="hover:border-primary/30 transition-colors">
|
||||
<div className="row gap-3 items-center">
|
||||
<div className="col gap-1 flex-1 min-w-0">
|
||||
<h4 className="text-base font-semibold group-hover:text-primary transition-colors">
|
||||
{alternative.name} alternative
|
||||
</h4>
|
||||
</div>
|
||||
<ArrowRightIcon className="opacity-0 group-hover:opacity-100 size-4 shrink-0 text-muted-foreground group-hover:text-primary group-hover:translate-x-1 transition-all duration-300" />
|
||||
</div>
|
||||
</FeatureCardContainer>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<h3 className="font-semibold text-muted-foreground text-sm uppercase tracking-wider">
|
||||
Comparisons
|
||||
</h3>
|
||||
{relatedLinks.alternatives.map((alternative) => (
|
||||
<Link
|
||||
className="row items-center gap-2 text-sm transition-colors hover:text-primary"
|
||||
href={alternative.url}
|
||||
key={alternative.url}
|
||||
>
|
||||
<ArrowRightIcon className="size-4 shrink-0" />
|
||||
{alternative.name} alternative
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { CheckIcon } from 'lucide-react';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import type { ForBenefits as ForBenefitsData } from '@/lib/for';
|
||||
|
||||
interface ForBenefitsProps {
|
||||
benefits: ForBenefitsData;
|
||||
}
|
||||
|
||||
export function ForBenefits({ benefits }: ForBenefitsProps) {
|
||||
return (
|
||||
<Section className="container">
|
||||
<SectionHeader
|
||||
description={benefits.intro}
|
||||
title={benefits.title}
|
||||
variant="sm"
|
||||
/>
|
||||
<div className="col mt-12 max-w-3xl gap-4">
|
||||
{benefits.items.map((benefit) => (
|
||||
<div className="row items-start gap-3" key={benefit.title}>
|
||||
<CheckIcon className="mt-0.5 size-5 shrink-0 text-green-500" />
|
||||
<div className="col gap-1">
|
||||
<h3 className="font-semibold">{benefit.title}</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{benefit.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { FaqItem, Faqs } from '@/components/faq';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import type { ForFaqs } from '@/lib/for';
|
||||
|
||||
interface ForFaqProps {
|
||||
faqs: ForFaqs;
|
||||
}
|
||||
|
||||
export function ForFaq({ faqs }: ForFaqProps) {
|
||||
return (
|
||||
<Section className="container">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<SectionHeader
|
||||
className="mb-16"
|
||||
description={faqs.intro}
|
||||
title={faqs.title}
|
||||
variant="sm"
|
||||
/>
|
||||
<Faqs>
|
||||
{faqs.items.map((faq) => (
|
||||
<FaqItem key={faq.question} question={faq.question}>
|
||||
{faq.answer}
|
||||
</FaqItem>
|
||||
))}
|
||||
</Faqs>
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { CheckCircle2Icon } from 'lucide-react';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import type { ForFeatures as ForFeaturesData } from '@/lib/for';
|
||||
|
||||
interface ForFeaturesProps {
|
||||
features: ForFeaturesData;
|
||||
}
|
||||
|
||||
export function ForFeatures({ features }: ForFeaturesProps) {
|
||||
return (
|
||||
<Section className="container">
|
||||
<SectionHeader
|
||||
description={features.intro}
|
||||
title={features.title}
|
||||
variant="sm"
|
||||
/>
|
||||
<div className="mt-12 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{features.items.map((feature) => (
|
||||
<div className="col gap-2 rounded-2xl border p-6" key={feature.title}>
|
||||
<div className="row items-center gap-2">
|
||||
<CheckCircle2Icon className="size-5 shrink-0 text-green-500" />
|
||||
<h3 className="font-semibold">{feature.title}</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{feature.description}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { CheckCircle2Icon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { HeroContainer } from '@/app/(home)/_sections/hero';
|
||||
import { GetStartedButton } from '@/components/get-started-button';
|
||||
import { Perks } from '@/components/perks';
|
||||
import { SectionHeader } from '@/components/section';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import type { ForHero as ForHeroData } from '@/lib/for';
|
||||
|
||||
interface ForHeroProps {
|
||||
hero: ForHeroData;
|
||||
tocItems?: Array<{ id: string; label: string }>;
|
||||
}
|
||||
|
||||
export function ForHero({ hero }: ForHeroProps) {
|
||||
return (
|
||||
<HeroContainer className="-mb-32" divider={false}>
|
||||
<div className="col gap-6">
|
||||
<SectionHeader
|
||||
as="h1"
|
||||
className="flex-1"
|
||||
description={hero.subheading}
|
||||
title={hero.heading}
|
||||
variant="sm"
|
||||
/>
|
||||
<div className="row gap-4">
|
||||
<GetStartedButton />
|
||||
<Button asChild size="lg" variant="outline">
|
||||
<Link
|
||||
href={'https://demo.openpanel.dev'}
|
||||
rel="noreferrer noopener nofollow"
|
||||
target="_blank"
|
||||
>
|
||||
See live demo
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<Perks
|
||||
className="flex flex-wrap gap-4"
|
||||
perks={hero.badges.map((badge) => ({
|
||||
text: badge,
|
||||
icon: CheckCircle2Icon,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</HeroContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { XCircleIcon } from 'lucide-react';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import type { ForProblem as ForProblemData } from '@/lib/for';
|
||||
|
||||
interface ForProblemProps {
|
||||
problem: ForProblemData;
|
||||
}
|
||||
|
||||
export function ForProblem({ problem }: ForProblemProps) {
|
||||
return (
|
||||
<Section className="container">
|
||||
<SectionHeader
|
||||
description={problem.intro}
|
||||
title={problem.title}
|
||||
variant="sm"
|
||||
/>
|
||||
<div className="mt-12 grid gap-6 md:grid-cols-2">
|
||||
{problem.items.map((item) => (
|
||||
<div className="col gap-2 rounded-2xl border p-6" key={item.title}>
|
||||
<div className="row items-center gap-2">
|
||||
<XCircleIcon className="size-5 shrink-0 text-red-500" />
|
||||
<h3 className="font-semibold">{item.title}</h3>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">{item.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Section, SectionHeader } from '@/components/section';
|
||||
import type { ForRelatedLinks } from '@/lib/for';
|
||||
|
||||
interface ForRelatedLinksProps {
|
||||
relatedLinks: ForRelatedLinks;
|
||||
}
|
||||
|
||||
export function ForRelatedLinksSection({ relatedLinks }: ForRelatedLinksProps) {
|
||||
const hasLinks =
|
||||
relatedLinks.articles?.length ||
|
||||
relatedLinks.guides?.length ||
|
||||
relatedLinks.comparisons?.length;
|
||||
|
||||
if (!hasLinks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section className="container">
|
||||
<SectionHeader
|
||||
description="Learn more about OpenPanel and how it can help you."
|
||||
title="Related resources"
|
||||
variant="sm"
|
||||
/>
|
||||
<div className="mt-12 grid gap-8 md:grid-cols-3">
|
||||
{relatedLinks.guides && relatedLinks.guides.length > 0 && (
|
||||
<div className="col gap-4">
|
||||
<h3 className="font-semibold text-muted-foreground text-sm uppercase tracking-wider">
|
||||
Guides
|
||||
</h3>
|
||||
{relatedLinks.guides.map((link) => (
|
||||
<Link
|
||||
className="row items-center gap-2 text-sm transition-colors hover:text-primary"
|
||||
href={link.url}
|
||||
key={link.url}
|
||||
>
|
||||
<ArrowRightIcon className="size-4 shrink-0" />
|
||||
{link.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{relatedLinks.articles && relatedLinks.articles.length > 0 && (
|
||||
<div className="col gap-4">
|
||||
<h3 className="font-semibold text-muted-foreground text-sm uppercase tracking-wider">
|
||||
Articles
|
||||
</h3>
|
||||
{relatedLinks.articles.map((link) => (
|
||||
<Link
|
||||
className="row items-center gap-2 text-sm transition-colors hover:text-primary"
|
||||
href={link.url}
|
||||
key={link.url}
|
||||
>
|
||||
<ArrowRightIcon className="size-4 shrink-0" />
|
||||
{link.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{relatedLinks.comparisons && relatedLinks.comparisons.length > 0 && (
|
||||
<div className="col gap-4">
|
||||
<h3 className="font-semibold text-muted-foreground text-sm uppercase tracking-wider">
|
||||
Comparisons
|
||||
</h3>
|
||||
{relatedLinks.comparisons.map((link) => (
|
||||
<Link
|
||||
className="row items-center gap-2 text-sm transition-colors hover:text-primary"
|
||||
href={link.url}
|
||||
key={link.url}
|
||||
>
|
||||
<ArrowRightIcon className="size-4 shrink-0" />
|
||||
{link.title}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
138
apps/public/src/app/(content)/for/[slug]/page.tsx
Normal file
138
apps/public/src/app/(content)/for/[slug]/page.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
import Script from 'next/script';
|
||||
import { ForBenefits } from './_components/for-benefits';
|
||||
import { ForFaq } from './_components/for-faq';
|
||||
import { ForFeatures } from './_components/for-features';
|
||||
import { ForHero } from './_components/for-hero';
|
||||
import { ForProblem } from './_components/for-problem';
|
||||
import { ForRelatedLinksSection } from './_components/for-related-links';
|
||||
import { CtaBanner } from '@/app/(home)/_sections/cta-banner';
|
||||
import { WindowImage } from '@/components/window-image';
|
||||
import { getAllForSlugs, getForData } from '@/lib/for';
|
||||
import { url } from '@/lib/layout.shared';
|
||||
import { getOgImageUrl, getPageMetadata } from '@/lib/metadata';
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const slugs = await getAllForSlugs();
|
||||
return slugs.map((slug) => ({ slug }));
|
||||
}
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}): Promise<Metadata> {
|
||||
const { slug } = await params;
|
||||
const data = await getForData(slug);
|
||||
|
||||
if (!data) {
|
||||
return {
|
||||
title: 'Page Not Found',
|
||||
};
|
||||
}
|
||||
|
||||
return getPageMetadata({
|
||||
title: data.seo.title,
|
||||
description: data.seo.description,
|
||||
url: data.url,
|
||||
image: getOgImageUrl(data.url),
|
||||
});
|
||||
}
|
||||
|
||||
export default async function ForPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
const data = await getForData(slug);
|
||||
|
||||
if (!data) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const pageUrl = url(`/for/${slug}`);
|
||||
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'WebPage',
|
||||
name: data.seo.title,
|
||||
description: data.seo.description,
|
||||
url: pageUrl,
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
name: 'OpenPanel',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: url('/logo.webp'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Script
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
id="for-schema"
|
||||
strategy="beforeInteractive"
|
||||
type="application/ld+json"
|
||||
/>
|
||||
<ForHero hero={data.hero} />
|
||||
|
||||
<div className="container my-16">
|
||||
<WindowImage
|
||||
alt="OpenPanel Dashboard Overview"
|
||||
caption="This is our web analytics dashboard, its an out-of-the-box experience so you can start understanding your traffic and engagement right away."
|
||||
srcDark="/screenshots/overview-dark.webp"
|
||||
srcLight="/screenshots/overview-light.webp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="problem">
|
||||
<ForProblem problem={data.problem} />
|
||||
</div>
|
||||
|
||||
<div id="features">
|
||||
<ForFeatures features={data.features} />
|
||||
</div>
|
||||
|
||||
<div className="container my-16">
|
||||
<WindowImage
|
||||
alt="OpenPanel Real-time Analytics"
|
||||
caption="Track events in real-time as they happen with instant updates and live monitoring."
|
||||
srcDark="/screenshots/realtime-dark.webp"
|
||||
srcLight="/screenshots/realtime-light.webp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="benefits">
|
||||
<ForBenefits benefits={data.benefits} />
|
||||
</div>
|
||||
|
||||
<div className="container my-16">
|
||||
<WindowImage
|
||||
alt="OpenPanel Dashboard"
|
||||
caption="Comprehensive analytics dashboard with real-time insights and customizable views."
|
||||
srcDark="/screenshots/dashboard-dark.webp"
|
||||
srcLight="/screenshots/dashboard-light.webp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div id="faq">
|
||||
<ForFaq faqs={data.faqs} />
|
||||
</div>
|
||||
|
||||
{data.related_links && (
|
||||
<ForRelatedLinksSection relatedLinks={data.related_links} />
|
||||
)}
|
||||
|
||||
<CtaBanner
|
||||
ctaLink={data.ctas.primary.href}
|
||||
ctaText={data.ctas.primary.label}
|
||||
description="Test OpenPanel free for 30 days, you'll not be charged anything unless you upgrade to a paid plan."
|
||||
title="Ready to get started?"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
57
apps/public/src/app/(content)/for/page.tsx
Normal file
57
apps/public/src/app/(content)/for/page.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { ArrowRightIcon } from 'lucide-react';
|
||||
import type { Metadata } from 'next';
|
||||
import Link from 'next/link';
|
||||
import { HeroContainer } from '@/app/(home)/_sections/hero';
|
||||
import { SectionHeader } from '@/components/section';
|
||||
import { getAllForSlugs, getForData } from '@/lib/for';
|
||||
import { getPageMetadata } from '@/lib/metadata';
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
return getPageMetadata({
|
||||
title: 'OpenPanel for Your Use Case',
|
||||
description:
|
||||
'Discover how OpenPanel helps startups, developers, and agencies with privacy-first, open-source web and product analytics.',
|
||||
url: '/for',
|
||||
});
|
||||
}
|
||||
|
||||
export default async function ForListPage() {
|
||||
const slugs = await getAllForSlugs();
|
||||
const pages = await Promise.all(
|
||||
slugs.map(async (slug) => {
|
||||
const data = await getForData(slug);
|
||||
return data;
|
||||
})
|
||||
);
|
||||
const validPages = pages.filter(
|
||||
(page): page is NonNullable<typeof page> => page !== null
|
||||
);
|
||||
|
||||
return (
|
||||
<HeroContainer divider={false}>
|
||||
<SectionHeader
|
||||
as="h1"
|
||||
description="See how OpenPanel helps different teams and industries with privacy-first, open-source analytics."
|
||||
title="OpenPanel for Your Use Case"
|
||||
variant="sm"
|
||||
/>
|
||||
<div className="mt-12 grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{validPages.map((page) => (
|
||||
<Link
|
||||
className="col gap-3 rounded-2xl border p-6 transition-colors hover:border-primary"
|
||||
href={`/for/${page.slug}`}
|
||||
key={page.slug}
|
||||
>
|
||||
<h2 className="font-semibold text-lg">{page.hero.heading}</h2>
|
||||
<p className="line-clamp-3 text-muted-foreground text-sm">
|
||||
{page.seo.description}
|
||||
</p>
|
||||
<div className="row mt-auto items-center gap-1 pt-2 text-primary text-sm">
|
||||
Learn more <ArrowRightIcon className="size-4" />
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</HeroContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,3 @@
|
||||
import {
|
||||
OPENPANEL_BASE_URL,
|
||||
OPENPANEL_DESCRIPTION,
|
||||
OPENPANEL_NAME,
|
||||
} from '@/lib/openpanel-brand';
|
||||
import { AnalyticsInsights } from './_sections/analytics-insights';
|
||||
import { Collaboration } from './_sections/collaboration';
|
||||
import { CtaBanner } from './_sections/cta-banner';
|
||||
@@ -13,19 +8,34 @@ import { Pricing } from './_sections/pricing';
|
||||
import { Sdks } from './_sections/sdks';
|
||||
import { Testimonials } from './_sections/testimonials';
|
||||
import { WhyOpenPanel } from './_sections/why-openpanel';
|
||||
import {
|
||||
OPENPANEL_BASE_URL,
|
||||
OPENPANEL_DESCRIPTION,
|
||||
OPENPANEL_NAME,
|
||||
OPENPANEL_SITE_NAME,
|
||||
} from '@/lib/openpanel-brand';
|
||||
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
{
|
||||
'@type': 'Organization',
|
||||
name: OPENPANEL_NAME,
|
||||
'@type': 'WebSite',
|
||||
name: OPENPANEL_SITE_NAME,
|
||||
alternateName: ['OpenPanel', 'OpenPanel.dev'],
|
||||
url: OPENPANEL_BASE_URL,
|
||||
sameAs: ['https://github.com/Openpanel-dev/openpanel'],
|
||||
description: OPENPANEL_DESCRIPTION,
|
||||
keywords: 'openpanel, product analytics, web analytics, mixpanel alternative, open source analytics',
|
||||
},
|
||||
|
||||
{
|
||||
'@type': 'Organization',
|
||||
name: OPENPANEL_SITE_NAME,
|
||||
url: OPENPANEL_BASE_URL,
|
||||
sameAs: [
|
||||
'https://github.com/Openpanel-dev/openpanel',
|
||||
'https://x.com/OpenPanelDev',
|
||||
],
|
||||
description: OPENPANEL_DESCRIPTION,
|
||||
keywords:
|
||||
'openpanel, product analytics, web analytics, mixpanel alternative, open source analytics',
|
||||
},
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
name: OPENPANEL_NAME,
|
||||
@@ -41,8 +51,8 @@ export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||
type="application/ld+json"
|
||||
/>
|
||||
<Hero />
|
||||
<WhyOpenPanel />
|
||||
|
||||
@@ -165,6 +165,7 @@ export interface RelatedLink {
|
||||
}
|
||||
|
||||
export interface RelatedLinks {
|
||||
guides?: RelatedLink[];
|
||||
articles?: RelatedLink[];
|
||||
alternatives?: RelatedLink[];
|
||||
}
|
||||
|
||||
114
apps/public/src/lib/for.ts
Normal file
114
apps/public/src/lib/for.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export interface ForSeo {
|
||||
title: string;
|
||||
description: string;
|
||||
noindex?: boolean;
|
||||
}
|
||||
|
||||
export interface ForHero {
|
||||
heading: string;
|
||||
subheading: string;
|
||||
badges: string[];
|
||||
}
|
||||
|
||||
export interface ForProblem {
|
||||
title: string;
|
||||
intro: string;
|
||||
items: Array<{
|
||||
title: string;
|
||||
description: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ForFeature {
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export interface ForFeatures {
|
||||
title: string;
|
||||
intro: string;
|
||||
items: ForFeature[];
|
||||
}
|
||||
|
||||
export interface ForBenefit {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface ForBenefits {
|
||||
title: string;
|
||||
intro: string;
|
||||
items: ForBenefit[];
|
||||
}
|
||||
|
||||
export interface ForFaq {
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
export interface ForFaqs {
|
||||
title: string;
|
||||
intro: string;
|
||||
items: ForFaq[];
|
||||
}
|
||||
|
||||
export interface ForCta {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface ForRelatedLinks {
|
||||
articles?: Array<{ title: string; url: string }>;
|
||||
guides?: Array<{ title: string; url: string }>;
|
||||
comparisons?: Array<{ title: string; url: string }>;
|
||||
}
|
||||
|
||||
export interface ForData {
|
||||
url: string;
|
||||
slug: string;
|
||||
audience: string;
|
||||
seo: ForSeo;
|
||||
hero: ForHero;
|
||||
problem: ForProblem;
|
||||
features: ForFeatures;
|
||||
benefits: ForBenefits;
|
||||
faqs: ForFaqs;
|
||||
related_links?: ForRelatedLinks;
|
||||
ctas: {
|
||||
primary: ForCta;
|
||||
secondary: ForCta;
|
||||
};
|
||||
}
|
||||
|
||||
const contentDir = join(process.cwd(), 'content', 'for');
|
||||
|
||||
export async function getForData(slug: string): Promise<ForData | null> {
|
||||
try {
|
||||
const filePath = join(contentDir, `${slug}.json`);
|
||||
const fileContents = readFileSync(filePath, 'utf8');
|
||||
const data = JSON.parse(fileContents) as ForData;
|
||||
return {
|
||||
...data,
|
||||
url: `/for/${slug}`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Error loading for data for ${slug}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllForSlugs(): Promise<string[]> {
|
||||
try {
|
||||
const files = readdirSync(contentDir);
|
||||
return files
|
||||
.filter((file) => file.endsWith('.json'))
|
||||
.map((file) => file.replace('.json', ''));
|
||||
} catch (error) {
|
||||
console.error('Error reading for directory:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { OPENPANEL_DESCRIPTION, OPENPANEL_NAME } from './openpanel-brand';
|
||||
import {
|
||||
OPENPANEL_DESCRIPTION,
|
||||
OPENPANEL_SITE_NAME,
|
||||
} from './openpanel-brand';
|
||||
import { url as baseUrl } from './layout.shared';
|
||||
|
||||
const siteName = OPENPANEL_NAME;
|
||||
const siteName = OPENPANEL_SITE_NAME;
|
||||
const defaultDescription = OPENPANEL_DESCRIPTION;
|
||||
const defaultImage = baseUrl('/ogimage.png');
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const OPENPANEL_NAME = 'OpenPanel';
|
||||
export const OPENPANEL_SITE_NAME = 'OpenPanel Analytics';
|
||||
export const OPENPANEL_BASE_URL = 'https://openpanel.dev';
|
||||
export const OPENPANEL_DESCRIPTION =
|
||||
'OpenPanel is an open-source web and product analytics platform, an open-source alternative to Mixpanel with optional self-hosting.';
|
||||
|
||||
Reference in New Issue
Block a user