public: seo work

This commit is contained in:
Carl-Gerhard Lindesvärd
2026-02-16 15:11:03 +01:00
parent 2f95c074c5
commit 4f4b4a8d88
8 changed files with 329 additions and 431 deletions

View File

@@ -1,41 +1,19 @@
import { FaqItem, Faqs } from '@/components/faq';
import { Section, SectionHeader } from '@/components/section';
import { CompareFaqs } from '@/lib/compare';
import Script from 'next/script';
import { url } from '@/lib/layout.shared';
import type { CompareFaqs } from '@/lib/compare';
interface CompareFaqProps {
faqs: CompareFaqs;
pageUrl: string;
}
export function CompareFaq({ faqs, pageUrl }: CompareFaqProps) {
const faqJsonLd = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.items.map((q) => ({
'@type': 'Question',
name: q.question,
acceptedAnswer: {
'@type': 'Answer',
text: q.answer,
},
})),
};
export function CompareFaq({ faqs }: CompareFaqProps) {
return (
<Section className="container">
<Script
strategy="beforeInteractive"
id="compare-faq-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<SectionHeader
className="mb-16"
title={faqs.title}
description={faqs.intro}
title={faqs.title}
variant="sm"
/>
<Faqs>
@@ -49,4 +27,3 @@ export function CompareFaq({ faqs, pageUrl }: CompareFaqProps) {
</Section>
);
}

View File

@@ -1,12 +1,3 @@
import { CtaBanner } from '@/app/(home)/_sections/cta-banner';
import { WindowImage } from '@/components/window-image';
import {
type CompareData,
getAllCompareSlugs,
getCompareData,
} from '@/lib/compare';
import { url } from '@/lib/layout.shared';
import { getOgImageUrl, getPageMetadata } from '@/lib/metadata';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import Script from 'next/script';
@@ -22,6 +13,11 @@ import { RelatedLinksSection } from './_components/related-links';
import { TechnicalComparison } from './_components/technical-comparison';
import { UseCases } from './_components/use-cases';
import { WhoShouldChoose } from './_components/who-should-choose';
import { CtaBanner } from '@/app/(home)/_sections/cta-banner';
import { WindowImage } from '@/components/window-image';
import { getAllCompareSlugs, getCompareData } from '@/lib/compare';
import { url } from '@/lib/layout.shared';
import { getOgImageUrl, getPageMetadata } from '@/lib/metadata';
export async function generateStaticParams() {
const slugs = await getAllCompareSlugs();
@@ -83,9 +79,7 @@ export default async function ComparePage({
// Build ToC items
const tocItems = [
...(data.overview
? [{ id: 'overview', label: data.overview.title }]
: []),
...(data.overview ? [{ id: 'overview', label: data.overview.title }] : []),
{ id: 'who-should-choose', label: data.summary_comparison.title },
{ id: 'comparison', label: data.highlights.title },
{ id: 'features', label: data.feature_comparison.title },
@@ -106,19 +100,19 @@ export default async function ComparePage({
return (
<div>
<Script
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
id="compare-schema"
strategy="beforeInteractive"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<CompareHero hero={data.hero} tocItems={tocItems} />
<div className="container my-16">
<WindowImage
srcDark="/screenshots/overview-dark.webp"
srcLight="/screenshots/overview-light.webp"
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>
@@ -130,25 +124,25 @@ export default async function ComparePage({
<div id="who-should-choose">
<WhoShouldChoose
summary={data.summary_comparison}
competitorName={data.competitor.name}
summary={data.summary_comparison}
/>
</div>
<div className="container my-16">
<WindowImage
srcDark="/screenshots/dashboard-dark.webp"
srcLight="/screenshots/dashboard-light.webp"
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="comparison">
<ComparisonTable
highlights={data.highlights}
featureComparison={data.feature_comparison}
competitorName={data.competitor.name}
featureComparison={data.feature_comparison}
highlights={data.highlights}
/>
</div>
<div id="features">
@@ -157,26 +151,26 @@ export default async function ComparePage({
<div className="container my-16">
<WindowImage
srcDark="/screenshots/realtime-dark.webp"
srcLight="/screenshots/realtime-light.webp"
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>
{data.technical_comparison && (
<div id="technical">
<TechnicalComparison
technical={data.technical_comparison}
competitorName={data.competitor.name}
technical={data.technical_comparison}
/>
</div>
)}
<div id="pricing">
<PricingSection
pricing={data.pricing}
competitorName={data.competitor.name}
pricing={data.pricing}
/>
</div>
@@ -192,10 +186,10 @@ export default async function ComparePage({
<div className="container my-16">
<WindowImage
srcDark="/screenshots/report-dark.webp"
srcLight="/screenshots/report-light.webp"
alt="OpenPanel Reports"
caption="Generate detailed reports and insights with customizable metrics and visualizations."
srcDark="/screenshots/report-dark.webp"
srcLight="/screenshots/report-light.webp"
/>
</div>
@@ -203,26 +197,26 @@ export default async function ComparePage({
<>
<div id="benefits">
<BenefitsSection
benefits={data.benefits_section.benefits}
cta={data.benefits_section.cta}
description={data.benefits_section.description}
label={data.benefits_section.label}
title={data.benefits_section.title}
description={data.benefits_section.description}
cta={data.benefits_section.cta}
benefits={data.benefits_section.benefits}
/>
</div>
<div className="container my-16">
<WindowImage
srcDark="/screenshots/profile-dark.webp"
srcLight="/screenshots/profile-light.webp"
alt="OpenPanel User Profiles"
caption="Deep dive into individual user profiles with complete event history and behavior tracking."
srcDark="/screenshots/profile-dark.webp"
srcLight="/screenshots/profile-light.webp"
/>
</div>
</>
)}
<div id="faq">
<CompareFaq faqs={data.faqs} pageUrl={pageUrl} />
<CompareFaq faqs={data.faqs} />
</div>
{data.related_links && (
@@ -230,10 +224,10 @@ export default async function ComparePage({
)}
<CtaBanner
title={'Ready to make the switch?'}
description="Test OpenPanel free for 30 days, you'll not be charged anything unless you upgrade to a paid plan."
ctaText={data.ctas.primary.label}
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 make the switch?'}
/>
</div>
);

View File

@@ -1,39 +1,19 @@
import { FaqItem, Faqs } from '@/components/faq';
import { Section, SectionHeader } from '@/components/section';
import type { FeatureFaqs } from '@/lib/features';
import Script from 'next/script';
interface FeatureFaqProps {
faqs: FeatureFaqs;
}
export function FeatureFaq({ faqs }: FeatureFaqProps) {
const faqJsonLd = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.items.map((q) => ({
'@type': 'Question',
name: q.question,
acceptedAnswer: {
'@type': 'Answer',
text: q.answer,
},
})),
};
return (
<Section className="container">
<Script
strategy="beforeInteractive"
id="feature-faq-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<SectionHeader
className="mb-16"
title={faqs.title}
description={faqs.intro}
title={faqs.title}
variant="sm"
/>
<Faqs>

View File

@@ -1,6 +1,5 @@
import { FaqItem, Faqs } from '@/components/faq';
import { Section, SectionHeader } from '@/components/section';
import Script from 'next/script';
const faqData = [
{
@@ -61,33 +60,13 @@ const faqData = [
];
export function Faq() {
const faqJsonLd = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqData.map((q) => ({
'@type': 'Question',
name: q.question,
acceptedAnswer: {
'@type': 'Answer',
text: q.answer,
},
})),
};
return (
<Section className="container">
<Script
strategy="beforeInteractive"
id="faq-schema"
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqJsonLd) }}
/>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<SectionHeader
className="mb-16"
title="FAQ"
description="Some of the most common questions we get asked."
title="FAQ"
/>
<Faqs>
{faqData.map((faq) => (

View File

@@ -1,4 +1,3 @@
import Script from 'next/script';
import Markdown from 'react-markdown';
import {
Accordion,
@@ -7,12 +6,22 @@ import {
AccordionTrigger,
} from './ui/accordion';
export const Faqs = ({ children }: { children: React.ReactNode }) => (
<div itemScope itemType="https://schema.org/FAQPage">
export const Faqs = ({
children,
schema = true,
}: {
children: React.ReactNode;
schema?: boolean;
}) => (
<div
{...(schema
? { itemScope: true, itemType: 'https://schema.org/FAQPage' }
: {})}
>
<Accordion
type="single"
className="w-full max-w-screen-md self-center rounded-3xl border bg-background-dark [&_button]:px-4 [&_div.answer]:bg-background-light"
collapsible
className="w-full max-w-screen-md self-center border rounded-3xl [&_button]:px-4 bg-background-dark [&_div.answer]:bg-background-light"
type="single"
>
{children}
</Accordion>
@@ -27,20 +36,20 @@ export const FaqItem = ({
children: string | React.ReactNode;
}) => (
<AccordionItem
value={question}
itemScope
itemProp="mainEntity"
itemType="https://schema.org/Question"
className="[&_[role=region]]:px-4"
itemProp="mainEntity"
itemScope
itemType="https://schema.org/Question"
value={question}
>
<AccordionTrigger className="text-left" itemProp="name">
{question}
</AccordionTrigger>
<AccordionContent
className="prose"
itemProp="acceptedAnswer"
itemScope
itemType="https://schema.org/Answer"
className="prose"
>
<div itemProp="text">
{typeof children === 'string' ? (